From 726bd0f0339042995190fd788c64e61f4936ad28 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Tue, 19 May 2026 02:54:06 -0700 Subject: [PATCH 1/3] refactor!: flatten triangulation modules into focused APIs - Move Delaunay-facing APIs from the old triangulation facade to crate-root modules and focused preludes such as construction, insertion, flips, repair, validation, and delaunayize. - Split generic Triangulation behavior into construction, insertion, query, orientation, repair, and validation modules with colocated tests and errors. - Keep the broad prelude for exploratory use while making focused preludes orthogonal and workflow-specific. - Refresh docs, examples, benchmarks, and API export tests for the new module layout and 7,500-vertex 3D debug-scale default. BREAKING CHANGE: Delaunay workflow imports now use crate-root modules and focused preludes instead of nested triangulation workflow paths. For example, use delaunay::prelude::construction::* instead of delaunay::prelude::triangulation::construction::*, and use root modules such as delaunay::construction, delaunay::repair, and delaunay::validation. BREAKING CHANGE: BistellarFlips now uses the const dimension as its trait parameter and exposes scalar and vertex-data types as associated types. Closes #381 --- README.md | 28 +- benches/PERFORMANCE_RESULTS.md | 124 +- benches/README.md | 2 +- benches/ci_performance_suite.rs | 14 +- benches/profiling_suite.rs | 6 +- benches/tds_clone.rs | 2 +- benches/topology_guarantee_construction.rs | 6 +- docs/ORIENTATION_SPEC.md | 2 +- docs/api_design.md | 30 +- docs/archive/issue_204_investigation.md | 2 +- docs/archive/todo_2026-04-23.md | 4 +- .../topology_integration_design_historical.md | 6 +- docs/code_organization.md | 50 +- docs/dev/debug_env_vars.md | 6 +- docs/dev/rust.md | 15 +- docs/dev/tooling-alignment.md | 6 + docs/diagnostics.md | 6 +- docs/invariants.md | 16 +- docs/limitations.md | 10 +- docs/numerical_robustness_guide.md | 10 +- ...production_review_remediation_checklist.md | 17 +- docs/roadmap.md | 4 +- docs/topology.md | 19 +- docs/validation.md | 38 +- docs/workflows.md | 37 +- examples/delaunayize_repair.rs | 8 +- examples/diagnostics.rs | 14 +- examples/numerical_robustness.rs | 8 +- examples/topology_editing.rs | 14 +- examples/triangulation_and_hull.rs | 8 +- justfile | 6 +- semgrep.yaml | 16 +- src/core/adjacency.rs | 2 +- src/core/algorithms/flips.rs | 48 +- src/core/algorithms/incremental_insertion.rs | 48 +- src/core/algorithms/pl_manifold_repair.rs | 8 +- src/core/boundary.rs | 2 +- src/core/collections/key_maps.rs | 4 +- src/core/collections/secondary_maps.rs | 4 +- src/core/construction.rs | 544 + src/core/facet.rs | 39 +- src/core/insertion.rs | 4596 ++++++ src/core/operations.rs | 24 +- src/core/orientation.rs | 781 + src/core/query.rs | 1182 ++ src/core/repair.rs | 1619 ++ src/core/simplex.rs | 54 +- src/core/tds.rs | 112 +- src/core/traits/data_type.rs | 2 +- src/core/traits/facet_cache.rs | 4 +- src/core/triangulation.rs | 12657 +--------------- src/core/util/deduplication.rs | 14 +- src/core/util/delaunay_validation.rs | 10 +- src/core/util/facet_keys.rs | 2 +- src/core/util/facet_utils.rs | 2 +- src/core/util/jaccard.rs | 4 +- src/core/validation.rs | 3020 ++++ src/core/vertex.rs | 30 +- src/{triangulation => delaunay}/builder.rs | 96 +- .../delaunay.rs => delaunay/construction.rs} | 11853 ++++----------- .../delaunayize.rs | 47 +- .../diagnostics.rs | 4 +- src/{triangulation => delaunay}/flips.rs | 134 +- src/delaunay/insertion.rs | 1335 ++ src/{triangulation => delaunay}/locality.rs | 8 +- src/delaunay/query.rs | 1308 ++ src/delaunay/repair.rs | 1468 ++ src/delaunay/serialization.rs | 184 + src/delaunay/triangulation.rs | 81 + src/delaunay/validation.rs | 951 ++ src/geometry/algorithms/convex_hull.rs | 101 +- src/geometry/kernel.rs | 4 +- src/geometry/quality.rs | 7 +- src/geometry/util/measures.rs | 2 +- src/geometry/util/triangulation_generation.rs | 18 +- src/lib.rs | 667 +- src/topology/manifold.rs | 10 +- src/triangulation.rs | 81 - src/triangulation/validation.rs | 129 - tests/README.md | 4 +- tests/allocation_api.rs | 6 +- tests/circumsphere_debug_tools.rs | 2 +- tests/dedup_batch_construction.rs | 2 +- tests/delaunay_edge_cases.rs | 8 +- tests/delaunay_incremental_insertion.rs | 6 +- tests/delaunay_repair_fallback.rs | 8 +- tests/delaunayize_workflow.rs | 10 +- tests/euler_characteristic.rs | 10 +- tests/example_workflows.rs | 8 +- tests/insert_with_statistics.rs | 6 +- tests/large_scale_debug.rs | 20 +- tests/pachner_roundtrip.rs | 6 +- tests/prelude_exports.rs | 161 +- tests/proptest_delaunay_triangulation.rs | 10 +- tests/proptest_euler_characteristic.rs | 4 +- tests/proptest_flips.rs | 8 +- tests/proptest_orientation.rs | 6 +- tests/proptest_triangulation.rs | 4 +- tests/proptest_vertex.rs | 2 +- tests/public_topology_api.rs | 4 +- tests/regressions.rs | 12 +- tests/semgrep/src/project_rules/rust_style.rs | 18 +- tests/serialization_vertex_preservation.rs | 6 +- tests/trait_bound_ergonomics.rs | 2 +- tests/triangulation_builder.rs | 14 +- 105 files changed, 21652 insertions(+), 22529 deletions(-) create mode 100644 src/core/construction.rs create mode 100644 src/core/insertion.rs create mode 100644 src/core/orientation.rs create mode 100644 src/core/query.rs create mode 100644 src/core/repair.rs create mode 100644 src/core/validation.rs rename src/{triangulation => delaunay}/builder.rs (97%) rename src/{triangulation/delaunay.rs => delaunay/construction.rs} (50%) rename src/{triangulation => delaunay}/delaunayize.rs (97%) rename src/{triangulation => delaunay}/diagnostics.rs (99%) rename src/{triangulation => delaunay}/flips.rs (78%) create mode 100644 src/delaunay/insertion.rs rename src/{triangulation => delaunay}/locality.rs (98%) create mode 100644 src/delaunay/query.rs create mode 100644 src/delaunay/repair.rs create mode 100644 src/delaunay/serialization.rs create mode 100644 src/delaunay/triangulation.rs create mode 100644 src/delaunay/validation.rs delete mode 100644 src/triangulation.rs delete mode 100644 src/triangulation/validation.rs diff --git a/README.md b/README.md index b0fd3d1e..4767db0b 100644 --- a/README.md +++ b/README.md @@ -172,16 +172,16 @@ Choose the smallest prelude that matches the task: | Task | Import | |---|---| -| Construct/configure a Delaunay triangulation | `use delaunay::prelude::triangulation::construction::*` | +| Construct/configure a Delaunay triangulation | `use delaunay::prelude::construction::*` | | Read-only traversal, adjacency, convex hulls, and comparison helpers | `use delaunay::prelude::query::*` | | Points, kernels, predicates, and geometric measures | `use delaunay::prelude::geometry::*` | | Random points or triangulations for examples, tests, and benchmarks | `use delaunay::prelude::generators::*` | -| Low-level incremental insertion building blocks | `use delaunay::prelude::triangulation::insertion::*` | -| Bistellar flips / Edit API | `use delaunay::prelude::triangulation::flips::*` | -| Delaunay repair diagnostics and policies | `use delaunay::prelude::triangulation::repair::*` | -| Delaunayize workflow | `use delaunay::prelude::triangulation::delaunayize::*` | -| Construction telemetry diagnostics | `use delaunay::prelude::triangulation::diagnostics::*` | -| Construction validation cadence/policy | `use delaunay::prelude::triangulation::validation::*` | +| Low-level incremental insertion building blocks | `use delaunay::prelude::insertion::*` | +| Bistellar flips / Edit API | `use delaunay::prelude::flips::*` | +| Delaunay repair diagnostics and policies | `use delaunay::prelude::repair::*` | +| Delaunayize workflow | `use delaunay::prelude::delaunayize::*` | +| Construction telemetry diagnostics | `use delaunay::prelude::diagnostics::*` | +| Construction validation cadence/policy | `use delaunay::prelude::validation::*` | | Hilbert ordering and quantization utilities | `use delaunay::prelude::ordering::*` | | Low-level TDS simplices, facets, keys, and validation reports | `use delaunay::prelude::tds::*` | | Collection aliases and small buffers | `use delaunay::prelude::collections::*` | @@ -190,7 +190,7 @@ Choose the smallest prelude that matches the task: `use delaunay::prelude::*` remains available for quick experiments, but examples and benchmarks in this repository prefer focused preludes so imports document -intent. The broad `delaunay::prelude::triangulation::*` import is retained for +intent. The broad `delaunay::prelude::*` import is retained for compatibility, but new docs and tests should prefer the narrow workflow preludes above. @@ -203,7 +203,7 @@ preludes. Contributors should follow the namespace policy in [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/code_organization.md](docs/code_organization.md). ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, }; @@ -235,7 +235,7 @@ fn main() -> Result<(), DelaunayTriangulationConstructionError> { For periodic boundary conditions, use `DelaunayTriangulationBuilder`: ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyKind, vertex, }; @@ -313,11 +313,11 @@ regression testing: `ValidationPolicy` ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ ConstructionOptions, DedupPolicy, DelaunayTriangulationBuilder, InsertionOrderStrategy, RetryPolicy, TopologyGuarantee, vertex, }; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::validation::ValidationPolicy; let vertices = vec![ vertex!([0.0, 0.0]), @@ -356,7 +356,7 @@ For reproducible checks in CI/local runs, use `just check`, `just test`, - **Large 4D+ batches:** thousands of 4D points can be expensive to investigate. Use release mode and the large-scale debug harness for characterization. -- **3D scale:** the default `just debug-large-scale-3d` helper uses 8,000 +- **3D scale:** the default `just debug-large-scale-3d` helper uses 7,500 vertices for the near-one-minute acceptance path. The 10,000-vertex run has also passed full Levels 1–4 validation as a heavier characterization probe; use `just debug-large-scale-3d 10000 1` for local numbers. @@ -549,4 +549,4 @@ Portions of this library were developed with the assistance of these AI tools: [PL-manifold]: https://en.wikipedia.org/wiki/Piecewise_linear_manifold [Delaunay repair]: https://link.springer.com/article/10.1007/BF01975867 [Pachner moves]: https://en.wikipedia.org/wiki/Pachner_move -[`DelaunayTriangulationBuilder`]: https://docs.rs/delaunay/latest/delaunay/triangulation/builder/struct.DelaunayTriangulationBuilder.html +[`DelaunayTriangulationBuilder`]: https://docs.rs/delaunay/latest/delaunay/builder/struct.DelaunayTriangulationBuilder.html diff --git a/benches/PERFORMANCE_RESULTS.md b/benches/PERFORMANCE_RESULTS.md index 16d90cca..d5b8715d 100644 --- a/benches/PERFORMANCE_RESULTS.md +++ b/benches/PERFORMANCE_RESULTS.md @@ -3,9 +3,9 @@ This file contains performance benchmarks and analysis for the delaunay library. The results are automatically generated and updated by the benchmark infrastructure. -- **Last Updated**: 2026-05-15 16:51:13 UTC +- **Last Updated**: 2026-05-19 09:26:07 UTC - **Generated By**: benchmark_utils.py -- **Git Commit**: 77ba50b23c6f22fbeaa6acf240bb144bbb1458f7 +- **Git Commit**: d89961cfdc4af5c354c78f4e5fe008f1f273c6e2 - **Hardware**: Apple M4 Max (16 cores) - **Memory**: 64.0 GB - **OS**: macOS @@ -15,37 +15,37 @@ The results are automatically generated and updated by the benchmark infrastruct ### Current Criterion Run Information -- **Date: 2026-05-15 16:48:12 UTC** -- **Git commit: 77ba50b23c6f22fbeaa6acf240bb144bbb1458f7** +- **Date: 2026-05-19 09:22:58 UTC** +- **Git commit: d89961cfdc4af5c354c78f4e5fe008f1f273c6e2** - **Source: current `target/criterion` construction results** ### 2D Triangulation Performance | Benchmark ID | Vertices | Time (mean) | Throughput (mean) | Simplices Generated | |--------------|--------|-------------|-------------------|---------------------| -| `tds_new_2d/tds_new/4000` | 4000 | 1072.154 ms | 3.731 Kelem/s | 7,974 | -| `tds_new_2d/tds_new_adversarial/4000` | 4000 | 1219.215 ms | 3.281 Kelem/s | 7,971 | +| `tds_new_2d/tds_new/4000` | 4000 | 1279.206 ms | 3.127 Kelem/s | 7,974 | +| `tds_new_2d/tds_new_adversarial/4000` | 4000 | 1435.454 ms | 2.787 Kelem/s | 7,971 | ### 3D Triangulation Performance | Benchmark ID | Vertices | Time (mean) | Throughput (mean) | Simplices Generated | |--------------|--------|-------------|-------------------|---------------------| -| `tds_new_3d/tds_new/750` | 750 | 800.463 ms | 0.937 Kelem/s | 4,663 | -| `tds_new_3d/tds_new_adversarial/750` | 750 | 1055.603 ms | 0.710 Kelem/s | 4,726 | +| `tds_new_3d/tds_new/750` | 750 | 827.636 ms | 0.906 Kelem/s | 4,663 | +| `tds_new_3d/tds_new_adversarial/750` | 750 | 1075.251 ms | 0.698 Kelem/s | 4,726 | ### 4D Triangulation Performance | Benchmark ID | Vertices | Time (mean) | Throughput (mean) | Simplices Generated | |--------------|--------|-------------|-------------------|---------------------| -| `tds_new_4d/tds_new/75` | 75 | 860.178 ms | 0.087 Kelem/s | 1,105 | -| `tds_new_4d/tds_new_adversarial/75` | 75 | 921.821 ms | 0.081 Kelem/s | 1,129 | +| `tds_new_4d/tds_new/75` | 75 | 866.594 ms | 0.087 Kelem/s | 1,105 | +| `tds_new_4d/tds_new_adversarial/75` | 75 | 931.844 ms | 0.080 Kelem/s | 1,129 | ### 5D Triangulation Performance | Benchmark ID | Vertices | Time (mean) | Throughput (mean) | Simplices Generated | |--------------|--------|-------------|-------------------|---------------------| -| `tds_new_5d/tds_new/25` | 25 | 975.732 ms | 0.026 Kelem/s | 421 | -| `tds_new_5d/tds_new_adversarial/25` | 25 | 852.341 ms | 0.029 Kelem/s | 410 | +| `tds_new_5d/tds_new/25` | 25 | 973.647 ms | 0.026 Kelem/s | 421 | +| `tds_new_5d/tds_new_adversarial/25` | 25 | 861.965 ms | 0.029 Kelem/s | 410 | ## Performance Results Summary @@ -61,14 +61,14 @@ Public API: `DelaunayTriangulation::new_with_options` | Benchmark ID | Dimension | Input | Variant | Mean | 95% CI | |--------------|-----------|-------|---------|------|--------| -| `tds_new_2d/tds_new/4000` | 2D | 4000 | well-conditioned | 1.072 s | 1.061 s - 1.092 s | -| `tds_new_2d/tds_new_adversarial/4000` | 2D | 4000 | adversarial | 1.219 s | 1.217 s - 1.222 s | -| `tds_new_3d/tds_new/750` | 3D | 750 | well-conditioned | 800.463 ms | 798.489 ms - 802.509 ms | -| `tds_new_3d/tds_new_adversarial/750` | 3D | 750 | adversarial | 1.056 s | 1.050 s - 1.061 s | -| `tds_new_4d/tds_new/75` | 4D | 75 | well-conditioned | 860.178 ms | 856.895 ms - 864.630 ms | -| `tds_new_4d/tds_new_adversarial/75` | 4D | 75 | adversarial | 921.821 ms | 918.663 ms - 925.314 ms | -| `tds_new_5d/tds_new/25` | 5D | 25 | well-conditioned | 975.732 ms | 969.279 ms - 982.384 ms | -| `tds_new_5d/tds_new_adversarial/25` | 5D | 25 | adversarial | 852.341 ms | 848.499 ms - 857.275 ms | +| `tds_new_2d/tds_new/4000` | 2D | 4000 | well-conditioned | 1.279 s | 1.243 s - 1.299 s | +| `tds_new_2d/tds_new_adversarial/4000` | 2D | 4000 | adversarial | 1.435 s | 1.398 s - 1.455 s | +| `tds_new_3d/tds_new/750` | 3D | 750 | well-conditioned | 827.636 ms | 826.885 ms - 828.336 ms | +| `tds_new_3d/tds_new_adversarial/750` | 3D | 750 | adversarial | 1.075 s | 1.073 s - 1.078 s | +| `tds_new_4d/tds_new/75` | 4D | 75 | well-conditioned | 866.594 ms | 863.077 ms - 871.017 ms | +| `tds_new_4d/tds_new_adversarial/75` | 4D | 75 | adversarial | 931.844 ms | 928.914 ms - 935.309 ms | +| `tds_new_5d/tds_new/25` | 5D | 25 | well-conditioned | 973.647 ms | 966.970 ms - 981.374 ms | +| `tds_new_5d/tds_new_adversarial/25` | 5D | 25 | adversarial | 861.965 ms | 859.260 ms - 864.511 ms | #### Boundary facets @@ -77,13 +77,13 @@ Public API: `DelaunayTriangulation::boundary_facets` | Benchmark ID | Dimension | Input | Variant | Mean | 95% CI | |--------------|-----------|-------|---------|------|--------| | `boundary_facets/boundary_facets_2d/4000` | 2D | 4000 | well-conditioned | 1.694 ms | 1.690 ms - 1.698 ms | -| `boundary_facets/boundary_facets_2d_adversarial/4000` | 2D | 4000 | adversarial | 1.687 ms | 1.684 ms - 1.690 ms | -| `boundary_facets/boundary_facets_3d/750` | 3D | 750 | well-conditioned | 1.478 ms | 1.473 ms - 1.483 ms | -| `boundary_facets/boundary_facets_3d_adversarial/750` | 3D | 750 | adversarial | 1.501 ms | 1.497 ms - 1.505 ms | -| `boundary_facets/boundary_facets_4d/75` | 4D | 75 | well-conditioned | 492.4 µs | 488.9 µs - 495.9 µs | -| `boundary_facets/boundary_facets_4d_adversarial/75` | 4D | 75 | adversarial | 508.3 µs | 503.6 µs - 513.0 µs | -| `boundary_facets/boundary_facets_5d/25` | 5D | 25 | well-conditioned | 239.1 µs | 236.8 µs - 241.3 µs | -| `boundary_facets/boundary_facets_5d_adversarial/25` | 5D | 25 | adversarial | 231.4 µs | 229.3 µs - 233.3 µs | +| `boundary_facets/boundary_facets_2d_adversarial/4000` | 2D | 4000 | adversarial | 1.687 ms | 1.685 ms - 1.690 ms | +| `boundary_facets/boundary_facets_3d/750` | 3D | 750 | well-conditioned | 1.489 ms | 1.484 ms - 1.494 ms | +| `boundary_facets/boundary_facets_3d_adversarial/750` | 3D | 750 | adversarial | 1.514 ms | 1.510 ms - 1.518 ms | +| `boundary_facets/boundary_facets_4d/75` | 4D | 75 | well-conditioned | 488.1 µs | 485.3 µs - 490.9 µs | +| `boundary_facets/boundary_facets_4d_adversarial/75` | 4D | 75 | adversarial | 500.4 µs | 497.2 µs - 503.6 µs | +| `boundary_facets/boundary_facets_5d/25` | 5D | 25 | well-conditioned | 235.9 µs | 233.9 µs - 237.9 µs | +| `boundary_facets/boundary_facets_5d_adversarial/25` | 5D | 25 | adversarial | 225.2 µs | 223.7 µs - 226.8 µs | #### Convex hull @@ -91,14 +91,14 @@ Public API: `ConvexHull::from_triangulation` | Benchmark ID | Dimension | Input | Variant | Mean | 95% CI | |--------------|-----------|-------|---------|------|--------| -| `convex_hull/from_triangulation_2d/4000` | 2D | 4000 | well-conditioned | 1.694 ms | 1.688 ms - 1.700 ms | -| `convex_hull/from_triangulation_2d_adversarial/4000` | 2D | 4000 | adversarial | 1.697 ms | 1.692 ms - 1.701 ms | -| `convex_hull/from_triangulation_3d/750` | 3D | 750 | well-conditioned | 1.486 ms | 1.480 ms - 1.493 ms | -| `convex_hull/from_triangulation_3d_adversarial/750` | 3D | 750 | adversarial | 1.499 ms | 1.493 ms - 1.506 ms | -| `convex_hull/from_triangulation_4d/75` | 4D | 75 | well-conditioned | 500.8 µs | 497.4 µs - 504.0 µs | -| `convex_hull/from_triangulation_4d_adversarial/75` | 4D | 75 | adversarial | 515.3 µs | 511.3 µs - 518.9 µs | -| `convex_hull/from_triangulation_5d/25` | 5D | 25 | well-conditioned | 244.7 µs | 242.5 µs - 246.7 µs | -| `convex_hull/from_triangulation_5d_adversarial/25` | 5D | 25 | adversarial | 236.3 µs | 234.9 µs - 237.8 µs | +| `convex_hull/from_triangulation_2d/4000` | 2D | 4000 | well-conditioned | 1.695 ms | 1.693 ms - 1.698 ms | +| `convex_hull/from_triangulation_2d_adversarial/4000` | 2D | 4000 | adversarial | 1.690 ms | 1.687 ms - 1.693 ms | +| `convex_hull/from_triangulation_3d/750` | 3D | 750 | well-conditioned | 1.481 ms | 1.477 ms - 1.484 ms | +| `convex_hull/from_triangulation_3d_adversarial/750` | 3D | 750 | adversarial | 1.514 ms | 1.510 ms - 1.518 ms | +| `convex_hull/from_triangulation_4d/75` | 4D | 75 | well-conditioned | 492.3 µs | 489.6 µs - 495.3 µs | +| `convex_hull/from_triangulation_4d_adversarial/75` | 4D | 75 | adversarial | 504.7 µs | 501.6 µs - 508.1 µs | +| `convex_hull/from_triangulation_5d/25` | 5D | 25 | well-conditioned | 238.9 µs | 237.1 µs - 240.6 µs | +| `convex_hull/from_triangulation_5d_adversarial/25` | 5D | 25 | adversarial | 227.0 µs | 224.8 µs - 229.0 µs | #### Validation @@ -106,12 +106,12 @@ Public API: `DelaunayTriangulation::validate` | Benchmark ID | Dimension | Input | Variant | Mean | 95% CI | |--------------|-----------|-------|---------|------|--------| -| `validation/validate_3d/750` | 3D | 750 | well-conditioned | 30.464 ms | 30.371 ms - 30.580 ms | -| `validation/validate_3d_adversarial/750` | 3D | 750 | adversarial | 40.098 ms | 40.018 ms - 40.179 ms | -| `validation/validate_4d/75` | 4D | 75 | well-conditioned | 67.646 ms | 67.481 ms - 67.810 ms | -| `validation/validate_4d_adversarial/75` | 4D | 75 | adversarial | 64.655 ms | 64.296 ms - 65.034 ms | -| `validation/validate_5d/25` | 5D | 25 | well-conditioned | 59.504 ms | 59.382 ms - 59.628 ms | -| `validation/validate_5d_adversarial/25` | 5D | 25 | adversarial | 55.897 ms | 55.699 ms - 56.101 ms | +| `validation/validate_3d/750` | 3D | 750 | well-conditioned | 30.343 ms | 30.097 ms - 30.680 ms | +| `validation/validate_3d_adversarial/750` | 3D | 750 | adversarial | 39.846 ms | 39.812 ms - 39.881 ms | +| `validation/validate_4d/75` | 4D | 75 | well-conditioned | 68.025 ms | 67.575 ms - 68.469 ms | +| `validation/validate_4d_adversarial/75` | 4D | 75 | adversarial | 64.382 ms | 64.151 ms - 64.702 ms | +| `validation/validate_5d/25` | 5D | 25 | well-conditioned | 60.577 ms | 60.316 ms - 60.814 ms | +| `validation/validate_5d_adversarial/25` | 5D | 25 | adversarial | 56.456 ms | 55.948 ms - 56.965 ms | #### Incremental insert @@ -119,14 +119,14 @@ Public API: `DelaunayTriangulation::insert` | Benchmark ID | Dimension | Input | Variant | Mean | 95% CI | |--------------|-----------|-------|---------|------|--------| -| `incremental_insert/insert_2d/10` | 2D | 10 | well-conditioned | 3.980 ms | 3.970 ms - 3.992 ms | -| `incremental_insert/insert_2d_adversarial/10` | 2D | 10 | adversarial | 6.396 ms | 6.377 ms - 6.414 ms | -| `incremental_insert/insert_3d/10` | 3D | 10 | well-conditioned | 5.497 ms | 5.470 ms - 5.537 ms | -| `incremental_insert/insert_3d_adversarial/10` | 3D | 10 | adversarial | 43.856 ms | 43.609 ms - 44.148 ms | -| `incremental_insert/insert_4d/6` | 4D | 6 | well-conditioned | 43.683 ms | 43.534 ms - 43.851 ms | -| `incremental_insert/insert_4d_adversarial/6` | 4D | 6 | adversarial | 140.643 ms | 140.325 ms - 140.943 ms | -| `incremental_insert/insert_5d/4` | 5D | 4 | well-conditioned | 593.467 ms | 591.065 ms - 596.128 ms | -| `incremental_insert/insert_5d_adversarial/4` | 5D | 4 | adversarial | 590.916 ms | 588.682 ms - 593.367 ms | +| `incremental_insert/insert_2d/10` | 2D | 10 | well-conditioned | 4.431 ms | 4.428 ms - 4.434 ms | +| `incremental_insert/insert_2d_adversarial/10` | 2D | 10 | adversarial | 6.829 ms | 6.816 ms - 6.844 ms | +| `incremental_insert/insert_3d/10` | 3D | 10 | well-conditioned | 5.761 ms | 5.741 ms - 5.781 ms | +| `incremental_insert/insert_3d_adversarial/10` | 3D | 10 | adversarial | 44.384 ms | 44.169 ms - 44.633 ms | +| `incremental_insert/insert_4d/6` | 4D | 6 | well-conditioned | 44.740 ms | 44.643 ms - 44.853 ms | +| `incremental_insert/insert_4d_adversarial/6` | 4D | 6 | adversarial | 142.597 ms | 141.929 ms - 143.245 ms | +| `incremental_insert/insert_5d/4` | 5D | 4 | well-conditioned | 596.594 ms | 594.594 ms - 599.018 ms | +| `incremental_insert/insert_5d_adversarial/4` | 5D | 4 | adversarial | 597.858 ms | 594.503 ms - 601.820 ms | #### Bistellar flips @@ -134,9 +134,9 @@ Public API: `BistellarFlips` | Benchmark ID | Dimension | Input | Variant | Mean | 95% CI | |--------------|-----------|-------|---------|------|--------| -| `bistellar_flips_4d/k1_roundtrip` | 4D | roundtrip | well-conditioned | 176.6 µs | 176.4 µs - 177.0 µs | -| `bistellar_flips_4d/k2_roundtrip` | 4D | roundtrip | well-conditioned | 185.4 µs | 185.0 µs - 185.8 µs | -| `bistellar_flips_4d/k3_roundtrip` | 4D | roundtrip | well-conditioned | 185.3 µs | 185.1 µs - 185.7 µs | +| `bistellar_flips_4d/k1_roundtrip` | 4D | roundtrip | well-conditioned | 179.4 µs | 178.2 µs - 180.4 µs | +| `bistellar_flips_4d/k2_roundtrip` | 4D | roundtrip | well-conditioned | 175.9 µs | 175.4 µs - 176.3 µs | +| `bistellar_flips_4d/k3_roundtrip` | 4D | roundtrip | well-conditioned | 176.9 µs | 176.5 µs - 177.2 µs | ### Circumsphere Predicate Performance @@ -150,32 +150,32 @@ insphere query performance independently from full triangulation workflows. | Test Case | insphere | insphere_distance | insphere_lifted | Winner | |-----------|----------|------------------|-----------------|---------| | Basic 2D | 17 ns | 23 ns | 8 ns | **insphere_lifted** | -| Boundary vertex | 1 ns | 24 ns | 194 ns | **insphere** | -| Far vertex | 17 ns | 24 ns | 8 ns | **insphere_lifted** | +| Boundary vertex | 1 ns | 25 ns | 210 ns | **insphere** | +| Far vertex | 17 ns | 25 ns | 8 ns | **insphere_lifted** | #### Single Query Performance (3D) | Test Case | insphere | insphere_distance | insphere_lifted | Winner | |-----------|----------|------------------|-----------------|---------| -| Basic 3D | 2.1 µs | 26 ns | 18 ns | **insphere_lifted** | -| Boundary vertex | 1 ns | 26 ns | 441 ns | **insphere** | +| Basic 3D | 2.2 µs | 26 ns | 18 ns | **insphere_lifted** | +| Boundary vertex | 1 ns | 26 ns | 430 ns | **insphere** | | Far vertex | 2.1 µs | 26 ns | 18 ns | **insphere_lifted** | #### Single Query Performance (4D) | Test Case | insphere | insphere_distance | insphere_lifted | Winner | |-----------|----------|------------------|-----------------|---------| -| Basic 4D | 5.2 µs | 54 ns | 2.9 µs | **insphere_distance** | -| Boundary vertex | 2 ns | 54 ns | 1.4 µs | **insphere** | -| Far vertex | 3.3 µs | 55 ns | 1.8 µs | **insphere_distance** | +| Basic 4D | 5.3 µs | 54 ns | 2.9 µs | **insphere_distance** | +| Boundary vertex | 2 ns | 55 ns | 1.4 µs | **insphere** | +| Far vertex | 3.4 µs | 54 ns | 1.8 µs | **insphere_distance** | #### Single Query Performance (5D) | Test Case | insphere | insphere_distance | insphere_lifted | Winner | |-----------|----------|------------------|-----------------|---------| -| Basic 5D | 8.4 µs | 81 ns | 4.8 µs | **insphere_distance** | -| Boundary vertex | 2 ns | 82 ns | 2.3 µs | **insphere** | -| Far vertex | 5.1 µs | 82 ns | 2.9 µs | **insphere_distance** | +| Basic 5D | 8.2 µs | 82 ns | 4.8 µs | **insphere_distance** | +| Boundary vertex | 2 ns | 81 ns | 2.3 µs | **insphere** | +| Far vertex | 5.1 µs | 81 ns | 2.9 µs | **insphere_distance** | ## Implementation Notes diff --git a/benches/README.md b/benches/README.md index e83761d5..fe99816f 100644 --- a/benches/README.md +++ b/benches/README.md @@ -264,7 +264,7 @@ dimension: | Dimension | Default vertices | Generated Simplices | Total wall time | |-----------|------------------|---------------------|-----------------| | 2D | 36,000 | 71,887 | ~48.1 s | -| 3D | 8,000 | 52,308 | ~51.9 s | +| 3D | 7,500 | Recomputed by harness | Near one minute | | 4D | 900 | 21,620 | ~57.4 s | | 5D | 140 | 8,296 | ~51.8 s | diff --git a/benches/ci_performance_suite.rs b/benches/ci_performance_suite.rs index bb5b14c4..b8750a1a 100644 --- a/benches/ci_performance_suite.rs +++ b/benches/ci_performance_suite.rs @@ -34,18 +34,18 @@ use criterion::measurement::WallTime; use criterion::{ BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, }; -use delaunay::prelude::generators::generate_random_points_seeded; -use delaunay::prelude::geometry::{ - AdaptiveKernel, Coordinate, Point, RobustKernel, simplex_volume, -}; -use delaunay::prelude::query::ConvexHull; -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ ConstructionOptions, DelaunayTriangulation, InsertionOrderStrategy, RetryPolicy, TopologyGuarantee, Vertex, }; -use delaunay::prelude::triangulation::flips::{ +use delaunay::prelude::flips::{ BistellarFlips, EdgeKey, FacetHandle, RidgeHandle, SimplexKey, TriangleHandle, }; +use delaunay::prelude::generators::generate_random_points_seeded; +use delaunay::prelude::geometry::{ + AdaptiveKernel, Coordinate, Point, RobustKernel, simplex_volume, +}; +use delaunay::prelude::query::ConvexHull; use delaunay::vertex; use std::{env, hint::black_box, num::NonZeroUsize, sync::Once}; #[cfg(feature = "bench-logging")] diff --git a/benches/profiling_suite.rs b/benches/profiling_suite.rs index 54fc3bdc..3adae7ad 100644 --- a/benches/profiling_suite.rs +++ b/benches/profiling_suite.rs @@ -76,12 +76,12 @@ use criterion::{ BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, }; use delaunay::prelude::collections::SmallBuffer; +use delaunay::prelude::construction::{ + ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationBuilder, RetryPolicy, Vertex, +}; use delaunay::prelude::generators::generate_random_points_seeded; use delaunay::prelude::geometry::{AdaptiveKernel, Coordinate, Point}; use delaunay::prelude::query::*; -use delaunay::prelude::triangulation::construction::{ - ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationBuilder, RetryPolicy, Vertex, -}; use delaunay::vertex; use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System, get_current_pid}; diff --git a/benches/tds_clone.rs b/benches/tds_clone.rs index 4ed045bc..a2faaee7 100644 --- a/benches/tds_clone.rs +++ b/benches/tds_clone.rs @@ -15,10 +15,10 @@ //! ``` use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use delaunay::prelude::construction::{DelaunayTriangulation, Vertex}; use delaunay::prelude::generators::generate_random_points_seeded; use delaunay::prelude::geometry::AdaptiveKernel; use delaunay::prelude::tds::Tds; -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, Vertex}; use std::hint::black_box; use std::time::Duration; diff --git a/benches/topology_guarantee_construction.rs b/benches/topology_guarantee_construction.rs index 3d3fa4a0..2145b5cb 100644 --- a/benches/topology_guarantee_construction.rs +++ b/benches/topology_guarantee_construction.rs @@ -15,10 +15,10 @@ //! ``` use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; +use delaunay::prelude::construction::{DelaunayTriangulation, TopologyGuarantee}; use delaunay::prelude::generators::generate_random_points_seeded; -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, TopologyGuarantee}; -use delaunay::prelude::triangulation::repair::DelaunayRepairPolicy; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::repair::DelaunayRepairPolicy; +use delaunay::prelude::validation::ValidationPolicy; use delaunay::vertex; use std::hint::black_box; use std::time::Duration; diff --git a/docs/ORIENTATION_SPEC.md b/docs/ORIENTATION_SPEC.md index 08be8fee..24b00ad3 100644 --- a/docs/ORIENTATION_SPEC.md +++ b/docs/ORIENTATION_SPEC.md @@ -194,7 +194,7 @@ drive repair, but replacement-simplex orientation itself uses `robust_orientatio ### Builder Paths -`src/triangulation/builder.rs` normalizes explicit and periodic construction: +`src/delaunay/builder.rs` normalizes explicit and periodic construction: - `from_vertices_and_simplices(...)` accepts user-provided simplex orderings, assembles the TDS, calls `normalize_and_promote_positive_orientation()`, validates TDS diff --git a/docs/api_design.md b/docs/api_design.md index f2a38406..ad750b41 100644 --- a/docs/api_design.md +++ b/docs/api_design.md @@ -14,7 +14,7 @@ The library provides two distinct APIs for different use cases: - Designed for building triangulations from point sets - Uses cavity-based insertion and fan retriangulation -2. **Edit API** (`prelude::triangulation::flips::BistellarFlips` trait) +2. **Edit API** (`prelude::flips::BistellarFlips` trait) - Low-level topology editing via bistellar (Pachner) flips - Explicit control over individual topology operations - Does **not** automatically restore the Delaunay property @@ -61,7 +61,7 @@ The library provides two distinct APIs for different use cases: For most use cases, the simple constructor is sufficient: ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; // Simple construction from vertices (Euclidean space, default options) let vertices = vec![ @@ -88,10 +88,10 @@ For advanced configuration (toroidal topology, custom validation policies, etc.) use `DelaunayTriangulationBuilder`: ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, TopologyGuarantee, vertex, }; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::validation::ValidationPolicy; // Toroidal (periodic) triangulation in 2D let vertices = vec![ @@ -149,11 +149,11 @@ for topology guarantee and validation policy details. ## Edit API Reference -The Edit API is exposed through the `BistellarFlips` trait in `prelude::triangulation::flips`: +The Edit API is exposed through the `BistellarFlips` trait in `prelude::flips`: ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -use delaunay::prelude::triangulation::flips::*; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::flips::*; // Start with a valid triangulation let vertices = vec![ @@ -261,8 +261,8 @@ After applying flips, you should: You can mix both APIs in the same workflow: ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -use delaunay::prelude::triangulation::flips::*; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::flips::*; // 1. Build initial triangulation (Builder API) let vertices = vec![ @@ -328,9 +328,9 @@ let report = dt.validation_report(); ### Internal Organization -- **Builder API**: Implemented in `triangulation::delaunay`, `triangulation::builder`, +- **Builder API**: Implemented in `delaunay::construction`, `delaunay::builder`, and `core::algorithms::incremental_insertion` -- **Edit API**: Implemented in `triangulation::flips` (public trait) and `core::algorithms::flips` (internal implementation) +- **Edit API**: Implemented in `delaunay::flips` (public trait) and `core::algorithms::flips` (internal implementation) - **Low-level primitives**: Context builders and flip application functions are `pub(crate)` in `core::algorithms::flips` ### Design Rationale @@ -352,12 +352,12 @@ See the following examples for practical demonstrations: ## Delaunayize Workflow -The `triangulation::delaunayize` module provides a single entrypoint for the +The `delaunay::delaunayize` module provides a single entrypoint for the common "repair topology then restore Delaunay" workflow: ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -use delaunay::prelude::triangulation::delaunayize::{ +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::delaunayize::{ DelaunayizeConfig, delaunayize_by_flips, }; @@ -417,4 +417,4 @@ changed or ambiguous simplices receive no payload. - **Validation framework**: See `docs/validation.md` for detailed validation guide - **Invariant rationale**: See [`invariants.md`](invariants.md) for theory and implementation pointers - **Topology analysis**: See `docs/topology.md` for topological concepts -- **API implementation**: See `triangulation::flips` module documentation +- **API implementation**: See `delaunay::flips` module documentation diff --git a/docs/archive/issue_204_investigation.md b/docs/archive/issue_204_investigation.md index a0c5c19a..cafb18c6 100644 --- a/docs/archive/issue_204_investigation.md +++ b/docs/archive/issue_204_investigation.md @@ -208,7 +208,7 @@ The dominant 85% hit N=50 (typical 5-cell seed set). ### Chosen Fix 2 constants -Declared as `pub(crate) const` in `src/triangulation/delaunay.rs`: +Declared as `pub(crate) const` in `src/delaunay/triangulation.rs`: - `LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_GE_4 = 12` (was inline `2`) - `LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_GE_4 = 96` (was inline `8`) diff --git a/docs/archive/todo_2026-04-23.md b/docs/archive/todo_2026-04-23.md index 72c1f955..5f542689 100644 --- a/docs/archive/todo_2026-04-23.md +++ b/docs/archive/todo_2026-04-23.md @@ -98,7 +98,7 @@ mutation, validation). ### ✅ ~~Module structure (#288)~~ — DONE -Completed in #317: `delaunay_triangulation.rs` → `triangulation/delaunay.rs`, +Completed in #317: `delaunay_triangulation.rs` → `delaunay/triangulation.rs`, `builder.rs` → `triangulation/builder.rs`. Public API preserved via re-exports. ### 🟢 Builder function size @@ -117,7 +117,7 @@ decomposed into smaller helpers. The prelude has 7+ sub-modules (`triangulation`, `geometry`, `query`, `collections`, `topology::validation`, `triangulation::flips`, -`triangulation::delaunayize`). New users may struggle to find the right +`delaunay::delaunayize`). New users may struggle to find the right import path. **Status:** documentation improvements are feasible (e.g., a diff --git a/docs/archive/topology_integration_design_historical.md b/docs/archive/topology_integration_design_historical.md index c31ebc46..b68900c2 100644 --- a/docs/archive/topology_integration_design_historical.md +++ b/docs/archive/topology_integration_design_historical.md @@ -656,7 +656,7 @@ impl CachedEulerCalculator { #[cfg(test)] mod euler_tests { use super::*; - use delaunay::prelude::triangulation::*; + use delaunay::prelude::*; #[test] fn test_2d_triangle_euler_characteristic() { @@ -715,7 +715,7 @@ mod euler_tests { #[cfg(test)] mod topology_validation_tests { use super::*; - use delaunay::prelude::triangulation::*; + use delaunay::prelude::*; #[test] fn test_planar_topology_validation_success() { @@ -764,7 +764,7 @@ mod topology_validation_tests { #[cfg(test)] mod random_topology_tests { use super::*; - use delaunay::prelude::triangulation::*; + use delaunay::prelude::*; use proptest::prelude::*; proptest! { diff --git a/docs/code_organization.md b/docs/code_organization.md index c6b91a94..282c76a5 100644 --- a/docs/code_organization.md +++ b/docs/code_organization.md @@ -184,12 +184,18 @@ delaunay/ │ │ │ └── uuid.rs │ │ ├── adjacency.rs │ │ ├── boundary.rs +│ │ ├── construction.rs │ │ ├── edge.rs │ │ ├── facet.rs +│ │ ├── insertion.rs │ │ ├── operations.rs +│ │ ├── orientation.rs +│ │ ├── query.rs +│ │ ├── repair.rs │ │ ├── simplex.rs │ │ ├── tds.rs │ │ ├── triangulation.rs +│ │ ├── validation.rs │ │ └── vertex.rs │ ├── geometry/ │ │ ├── algorithms/ @@ -222,16 +228,21 @@ delaunay/ │ │ │ ├── global_topology_model.rs │ │ │ └── topological_space.rs │ │ └── manifold.rs -│ ├── triangulation/ +│ ├── delaunay/ │ │ ├── builder.rs -│ │ ├── delaunay.rs +│ │ ├── construction.rs │ │ ├── delaunayize.rs │ │ ├── diagnostics.rs │ │ ├── flips.rs +│ │ ├── insertion.rs │ │ ├── locality.rs +│ │ ├── query.rs +│ │ ├── repair.rs +│ │ ├── serialization.rs +│ │ ├── triangulation.rs │ │ └── validation.rs │ ├── lib.rs -│ └── triangulation.rs +│ └── ... ├── tests/ │ ├── semgrep/ │ │ ├── .github/ @@ -402,6 +413,17 @@ The `benchmark-utils` CLI provides integrated benchmark workflow functionality, - `tds.rs` - Main `Tds` struct - `triangulation.rs` - Generic Triangulation layer with kernel +- `construction.rs` - Generic triangulation construction helpers and initial-simplex setup +- `insertion.rs` - Generic transactional insertion, duplicate detection, and insertion telemetry +- `orientation.rs` - Generic simplex orientation validation, lifted-coordinate + handling, and positive-orientation canonicalization +- `query.rs` - Read-only generic triangulation accessors, adjacency indices, + and topology traversal helpers +- `repair.rs` - Generic local topology repair, stale incident-simplex repair, + and vertex-removal cavity retriangulation +- `validation.rs` - Generic validation vocabulary and Level 3 orchestration; + Level 1 remains with `vertex.rs`/`simplex.rs`, Level 2 with `tds.rs`, and + Delaunay Level 4 with `src/delaunay/validation.rs` - `vertex.rs`, `simplex.rs`, `facet.rs` - Core geometric primitives - `edge.rs` - Canonical `EdgeKey` for topology traversal - `adjacency.rs` - Optional `AdjacencyIndex` builder outputs (opt-in) @@ -449,19 +471,27 @@ exposed through curated modules and focused preludes (`delaunay::tds`, - `point_generation.rs` - Random point generation (uniform, grid, Poisson disk sampling) - `triangulation_generation.rs` - Random triangulation generation with topology guarantees -**`src/triangulation/`** - Triangulation-facing public APIs: +**`src/delaunay/`** - Delaunay-facing implementation modules: - `builder.rs` - Fluent builder API for Euclidean and toroidal/periodic construction -- `delaunay.rs` - `DelaunayTriangulation` implementation (top layer) with incremental insertion +- `construction.rs` - Batch construction options, errors, statistics, and + high-level constructors +- `insertion.rs` - Post-construction vertex insertion/removal and repair policy orchestration +- `query.rs` - Read-only `DelaunayTriangulation` accessors and traversal helpers +- `triangulation.rs` - `DelaunayTriangulation` storage type and insertion-state cache - `delaunayize.rs` - End-to-end "repair then delaunayize" workflow (`delaunayize_by_flips`); bounded topology repair + flip-based Delaunay repair + optional fallback rebuild - `flips.rs` - High-level bistellar flip (Pachner move) trait and supporting public types; delegates to `core::algorithms::flips` - `locality.rs` - Local seed/frontier helpers for Hilbert-local construction and repair -- `validation.rs` - Construction validation cadence and scheduling helpers - -**`src/triangulation.rs`** - Public facade for triangulation-facing workflows. -It keeps the module namespace stable while the implementation is split across -orthogonal files under `src/triangulation/`. +- `repair.rs` - Delaunay repair policies, heuristic rebuild config, and repair outcomes +- `serialization.rs` - Conversion to/from `Tds` with topology metadata reset rules +- `validation.rs` - Level 4 validation errors plus construction validation cadence helpers + +**`src/lib.rs`** - Crate root, public module declarations, root re-exports, and +focused preludes. Delaunay-facing modules are exposed directly as +`delaunay::builder`, `delaunay::construction`, `delaunay::flips`, +`delaunay::repair`, `delaunay::validation`, and related focused preludes rather +than through a `delaunay::delaunay` or `delaunay::triangulation` facade. **`src/topology/`** - Topology analysis and validation: diff --git a/docs/dev/debug_env_vars.md b/docs/dev/debug_env_vars.md index d372dc1a..255e542f 100644 --- a/docs/dev/debug_env_vars.md +++ b/docs/dev/debug_env_vars.md @@ -41,11 +41,11 @@ This matters for large-scale investigations that need to run under | Variable | Activation | Module | Description | |---|---|---|---| | `DELAUNAY_INSERT_TRACE` | presence | `triangulation.rs` | `[release]` Per-insertion summary (vertex index, location, conflict size, suspicion flags) | -| `DELAUNAY_BULK_PROGRESS_EVERY` | **value** (integer) | `triangulation/delaunay.rs` | `[release]` Periodic batch progress for post-simplex bulk vertices. | +| `DELAUNAY_BULK_PROGRESS_EVERY` | **value** (integer) | `delaunay/triangulation.rs` | `[release]` Periodic batch progress for post-simplex bulk vertices. | | `DELAUNAY_DEBUG_CAVITY_REDUCTION_ONCE` | presence | `triangulation.rs` | `[release]` One-shot trace of first cavity reduction chain + re-extractions. | | `DELAUNAY_DEBUG_RETRYABLE_SKIP` | presence | `triangulation.rs` | `[release]` Retryable conflict skip trace with attempt and rollback context. | | `DELAUNAY_DEBUG_SHUFFLE` | presence | `triangulation.rs` | Logs vertex shuffle order during batch construction | -| `DELAUNAY_DUPLICATE_METRICS` | presence | `triangulation/delaunay.rs` | `[release]` Duplicate-detection metrics (spatial hash grid stats) | +| `DELAUNAY_DUPLICATE_METRICS` | presence | `delaunay/triangulation.rs` | `[release]` Duplicate-detection metrics (spatial hash grid stats) | ## Point Location @@ -76,7 +76,7 @@ This matters for large-scale investigations that need to run under | Variable | Activation | Module | Description | |---|---|---|---| -| `DELAUNAY_DEBUG_ORIENTATION` | presence | `triangulation.rs` | Negative-orientation simplex canonicalization and post-insertion audit | +| `DELAUNAY_DEBUG_ORIENTATION` | presence | `orientation.rs` | Negative-orientation simplex canonicalization and post-insertion audit | ## Neighbor Wiring diff --git a/docs/dev/rust.md b/docs/dev/rust.md index 6226bca7..64aa57f3 100644 --- a/docs/dev/rust.md +++ b/docs/dev/rust.md @@ -562,13 +562,14 @@ because they make intent visible at the import site. Examples: ```text +delaunay::prelude delaunay::prelude::triangulation -delaunay::prelude::triangulation::construction -delaunay::prelude::triangulation::flips -delaunay::prelude::triangulation::insertion -delaunay::prelude::triangulation::repair -delaunay::prelude::triangulation::delaunayize -delaunay::prelude::triangulation::validation +delaunay::prelude::construction +delaunay::prelude::flips +delaunay::prelude::insertion +delaunay::prelude::repair +delaunay::prelude::delaunayize +delaunay::prelude::validation delaunay::prelude::query delaunay::prelude::algorithms delaunay::prelude::geometry @@ -603,7 +604,7 @@ Example: /// # Examples /// /// ```rust -/// # use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; +/// # use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; /// # fn main() -> Result<(), Box> { /// let mut triangulation = DelaunayTriangulation::<_, _, _, 2>::default(); /// let key = triangulation.insert_vertex([0.0, 0.0])?; diff --git a/docs/dev/tooling-alignment.md b/docs/dev/tooling-alignment.md index 251368b6..0c02874e 100644 --- a/docs/dev/tooling-alignment.md +++ b/docs/dev/tooling-alignment.md @@ -178,6 +178,12 @@ The following previously deferred checks are now repository-owned Semgrep rules: - `delaunay.rust.no-silent-conversion-fallbacks-in-public-samples` extends the existing source conversion-fallback check to examples, benchmarks, and public API tests so copied usage does not hide numeric conversion failures. +- `delaunay.rust.prefer-prelude-imports-in-examples-benches` and + `delaunay.rust.prefer-prelude-imports-in-delaunay-doctests` track the + flattened Delaunay API surface: the removed `delaunay::delaunay::*` facade is + no longer matched, while focused root modules such as `delaunay::flips::*` + still trigger guidance toward the orthogonal prelude modules in public + samples. ## Public Sample Error Handling diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 40ecd2d2..05f503b2 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -19,7 +19,7 @@ Always available: - Typed construction, insertion, validation, topology, and repair errors. - Repair diagnostics attached to non-convergence and repair-neighbor failures. - Construction statistics and telemetry through - `delaunay::prelude::triangulation::diagnostics`. + `delaunay::prelude::diagnostics`. Feature-gated with `diagnostics`: @@ -65,7 +65,7 @@ delaunay = { version = "...", features = ["diagnostics"] } For most validation work, start with the always-available APIs: ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -108,7 +108,7 @@ empty-circumsphere violations: ```rust use delaunay::prelude::diagnostics::delaunay_violation_report; -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), diff --git a/docs/invariants.md b/docs/invariants.md index 975abba1..c51b56ab 100644 --- a/docs/invariants.md +++ b/docs/invariants.md @@ -215,15 +215,15 @@ simplicial complexes for geometry: global consistency check that catches some classes of topological corruption. Piecewise-linear (PL) manifoldness is strictly stronger than the pseudomanifold conditions. The public API exposes this -via [`TopologyGuarantee`](https://docs.rs/delaunay/latest/delaunay/core/triangulation/enum.TopologyGuarantee.html) -(source: [`src/core/triangulation.rs`](../src/core/triangulation.rs)): +via `TopologyGuarantee`, re-exported at the crate root and in +`delaunay::prelude::construction` (source: +[`src/core/validation.rs`](../src/core/validation.rs)): -- [`TopologyGuarantee::Pseudomanifold`](https://docs.rs/delaunay/latest/delaunay/core/triangulation/enum.TopologyGuarantee.html#variant.Pseudomanifold) +- `TopologyGuarantee::Pseudomanifold` checks the codimension-1 incidence conditions (plus boundary consistency, connectedness, isolated-vertex, and Euler characteristic checks). -- [`TopologyGuarantee::PLManifold`](https://docs.rs/delaunay/latest/delaunay/core/triangulation/enum.TopologyGuarantee.html#variant.PLManifold) - and - [`TopologyGuarantee::PLManifoldStrict`](https://docs.rs/delaunay/latest/delaunay/core/triangulation/enum.TopologyGuarantee.html#variant.PLManifoldStrict) +- `TopologyGuarantee::PLManifold` and + `TopologyGuarantee::PLManifoldStrict` add **link-based** conditions (ridge links and/or vertex links) that are characteristic of PL-manifolds. In PL topology, requiring the links of simplices to be spheres (or balls at the boundary) is equivalent to the standard manifold condition that every point has a locally @@ -258,7 +258,7 @@ vertex and verifying topological properties of the resulting complex. For this reason, the `delaunay` crate defers vertex-link validation until construction completion by default. When stronger guarantees are required, -[`TopologyGuarantee::PLManifoldStrict`](https://docs.rs/delaunay/latest/delaunay/core/triangulation/enum.TopologyGuarantee.html#variant.PLManifoldStrict) +`TopologyGuarantee::PLManifoldStrict` enables vertex-link validation after every insertion, trading performance for earlier detection and improved diagnosability. @@ -320,7 +320,7 @@ At a high level: - **Vertex-link validation** is stronger but significantly more expensive. The default strategy is to defer full vertex-link certification until construction completion. - **Strict mode** - ([`TopologyGuarantee::PLManifoldStrict`](https://docs.rs/delaunay/latest/delaunay/core/triangulation/enum.TopologyGuarantee.html#variant.PLManifoldStrict)) + (`TopologyGuarantee::PLManifoldStrict`) runs vertex-link validation after each insertion, trading performance for earlier detection and improved diagnosability. diff --git a/docs/limitations.md b/docs/limitations.md index f1a96696..2496c510 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -100,7 +100,7 @@ release characterization, not a portable performance promise: generation, batch construction, final flip repair, and `validation_report` for Levels 1–4. - The `just` helper defaults are dimension-aware rather than identical: 2D - defaults to 36,000 vertices, 3D defaults to 8,000 vertices, 4D defaults to + defaults to 36,000 vertices, 3D defaults to 7,500 vertices, 4D defaults to 900 vertices, and 5D defaults to 140 vertices. Pass `n` explicitly when a run must match a documented scale exactly. - The raw ignored tests use slightly heavier defaults for some dimensions @@ -119,10 +119,10 @@ Current 2D scale envelope: Current 3D scale envelope: -- `just debug-large-scale-3d 8000 1` is the current release-mode acceptance - harness for the 8,000-vertex 3D path. -- Recent maintainer-hardware runs insert all 8,000 vertices with zero skips, - run a clean final flip repair, and pass `validation_report` for Levels 1–4. +- `just debug-large-scale-3d 7500 1` is the current release-mode acceptance + harness for the 7,500-vertex 3D path. +- This helper is the default near-one-minute acceptance/profiling target for + final flip repair and `validation_report` coverage across Levels 1–4. - Wall time is hardware- and load-sensitive. Recent Apple M4 Max-class local runs complete in roughly 56 seconds; treat that as an envelope, not a portable guarantee. diff --git a/docs/numerical_robustness_guide.md b/docs/numerical_robustness_guide.md index 5e754fe1..3cec3125 100644 --- a/docs/numerical_robustness_guide.md +++ b/docs/numerical_robustness_guide.md @@ -109,7 +109,7 @@ The convenience constructors (`DelaunayTriangulation::new()`, `::empty()`, etc.) ```rust use delaunay::prelude::geometry::RobustKernel; -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; let kernel = RobustKernel::::new(); @@ -186,8 +186,8 @@ cases involve cavity/topology failures rather than predicate degeneracies. Use `insert_with_statistics()` to observe this behavior: ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -use delaunay::prelude::triangulation::insertion::InsertionOutcome; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::insertion::InsertionOutcome; let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); @@ -270,7 +270,7 @@ the vast majority of exact and near-duplicate vertices before any insertion occurs, regardless of `DedupPolicy`. See `order_vertices_hilbert` (called from `order_vertices_by_strategy`) in -[`src/triangulation/delaunay.rs`](../src/triangulation/delaunay.rs). +[`src/delaunay/triangulation.rs`](../src/delaunay/triangulation.rs). ### Layer 2: Per-insertion duplicate coordinate check @@ -316,7 +316,7 @@ See `validate_simplex_coordinate_uniqueness` in ### User-facing dedup utilities For explicit preprocessing, the crate provides public deduplication functions in -`delaunay::prelude::triangulation`: +`delaunay::prelude`: - `dedup_vertices_exact(&[Vertex])` — removes exact coordinate duplicates (O(n²)) - `dedup_vertices_epsilon(&[Vertex], epsilon)` — removes near-duplicates within diff --git a/docs/production_review_remediation_checklist.md b/docs/production_review_remediation_checklist.md index 54dd85b2..143171e1 100644 --- a/docs/production_review_remediation_checklist.md +++ b/docs/production_review_remediation_checklist.md @@ -28,10 +28,11 @@ Treat partial items as still open until their acceptance notes are satisfied. ## High-Value Improvements -- [ ] **6. Split very large source files.** - Start with `core/algorithms/flips.rs`, then `triangulation/delaunay.rs`, - `core/triangulation.rs`, and `core/tds.rs`. The triangulation-facing module - split is tracked for v0.7.8 in #381. +- [x] **6. Split very large source files.** + `core/triangulation.rs` and the Delaunay-facing layer have been split into + orthogonal construction, insertion, query, repair, orientation, validation, + and serialization modules. The remaining large-file targets are + `core/algorithms/flips.rs` and `core/tds.rs`. - [ ] **7. Replace full-TDS clone rollback with journaled or localized rollback.** This remains the largest performance opportunity. Tracked for v0.8.0 in #364. @@ -53,9 +54,11 @@ Treat partial items as still open until their acceptance notes are satisfied. - [ ] **13. Make strict insphere consistency test control isolated.** Rename the once-init env flag for process-wide semantics or use an atomic test hook. Tracked for v0.7.8 in #383. -- [ ] **14. Consolidate focused preludes.** - Reduce overlap and make import surfaces more orthogonal. Folded into the - v0.7.8 triangulation-module cleanup in #381. +- [x] **14. Consolidate focused preludes.** + Delaunay-facing workflow preludes now live directly under + `delaunay::prelude::{construction,insertion,flips,repair,delaunayize,diagnostics,validation}`, + while `delaunay::prelude::triangulation` is scoped to the generic + `Triangulation` layer. - [x] **15. Audit FastHashMap exposure to attacker-controlled hash keys.** Coordinate-derived hash-grid and epsilon-dedup buckets now use randomized `SecureHashMap`; remaining `FastHashMap` keys are slot keys, UUID identities, diff --git a/docs/roadmap.md b/docs/roadmap.md index 4fe53671..36ec738d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -36,7 +36,7 @@ Key takeaways from v0.7.7: suite. - **Module and prelude cleanup (#381):** rename the public high-level triangulation module layout to `delaunay`, split the current - `triangulation/delaunay.rs` implementation into clearer components, and + `delaunay/triangulation.rs` implementation into clearer components, and consolidate focused preludes while the API churn is still isolated from v0.8.0 feature work. - **Documentation and doctest hygiene (#214/#365):** move configuration-heavy @@ -87,7 +87,7 @@ tightly coupled to the v0.8.0 paper/API push: performance work is measured across the supported small-dimensional range instead of tuned for one dimension at another's expense. The current defaults are calibrated as roughly one-minute release-mode runs on maintainer hardware: - 2D=36,000, 3D=8,000, 4D=900, and 5D=140. Heavier explicit probes such as + 2D=36,000, 3D=7,500, 4D=900, and 5D=140. Heavier explicit probes such as 2D=40,000, 3D=10,000, and 5D=150 remain useful for release characterization. - **Criterion performance canaries:** keep smaller `ci_performance_suite` canaries for the same construction path so PR regression checks remain diff --git a/docs/topology.md b/docs/topology.md index b3aac05e..57f58916 100644 --- a/docs/topology.md +++ b/docs/topology.md @@ -18,7 +18,13 @@ Relevant modules (lexicographically sorted): ```text src/ ├── core/ -│ └── triangulation.rs +│ ├── query.rs +│ ├── triangulation.rs +│ └── validation.rs +├── delaunay/ +│ ├── flips.rs +│ ├── repair.rs +│ └── validation.rs ├── geometry/ ├── lib.rs ├── topology/ @@ -33,8 +39,6 @@ src/ │ └── traits/ │ ├── global_topology_model.rs │ └── topological_space.rs -└── triangulation/ - └── flips.rs ``` Notes: @@ -78,7 +82,8 @@ Level 3 always checks: Implementation pointers: -- Level 3 entry point: `src/core/triangulation.rs` (`Triangulation::is_valid`) +- Level 3 entry points and validation vocabulary: `src/core/validation.rs` + (`Triangulation::is_valid`, `Triangulation::validate`) - Manifold validators: `src/topology/manifold.rs` - Euler characteristic helpers: `src/topology/characteristics/{euler.rs,validation.rs}` @@ -177,7 +182,7 @@ Toroidal (periodic) triangulations are **fully implemented and functional**. You construct toroidal triangulations using `DelaunayTriangulationBuilder`: ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulationBuilder, vertex}; +use delaunay::prelude::construction::{DelaunayTriangulationBuilder, vertex}; // 2D periodic triangulation let vertices = vec![ @@ -205,9 +210,9 @@ Spherical and hyperbolic topologies are defined in metadata/behavior-model layers but are not yet fully integrated with the construction and validation pipeline. -## Triangulation editing (`src/triangulation/`) +## Triangulation editing (`src/delaunay/`) -`src/triangulation/flips.rs` exposes explicit bistellar-flip editing APIs +`src/delaunay/flips.rs` exposes explicit bistellar-flip editing APIs (`BistellarFlips`) built on `core::algorithms::flips`. These operations: - are topological edits (they can change manifold structure), and diff --git a/docs/validation.md b/docs/validation.md index f9124db4..489002c8 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -79,10 +79,10 @@ insertion deviates from the happy-path and trips internal **suspicion flags**, e ### Example: configuring validation policy ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulation, TopologyGuarantee, Vertex, vertex, }; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::validation::ValidationPolicy; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -124,10 +124,10 @@ PL-manifoldness. You can trigger that final certification via `Triangulation::validate_at_completion()` (or `Triangulation::validate()`). ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulation, TopologyGuarantee, Vertex, vertex, }; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::validation::ValidationPolicy; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -216,10 +216,10 @@ Validates basic data integrity of individual vertices and simplices. ### Example ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulation, TopologyGuarantee, Vertex, vertex, }; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::validation::ValidationPolicy; let v = vertex!([0.0, 0.0, 0.0]); assert!(v.is_valid().is_ok()); @@ -272,10 +272,10 @@ Validates the combinatorial structure of the Triangulation Data Structure. ### Example ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulation, TopologyGuarantee, Vertex, vertex, }; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::validation::ValidationPolicy; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -353,10 +353,10 @@ Validates that the triangulation forms a valid topological manifold. ### Example ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulation, TopologyGuarantee, Vertex, vertex, }; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::validation::ValidationPolicy; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -419,10 +419,10 @@ Validates the geometric optimality of the triangulation. ### Example ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulation, TopologyGuarantee, Vertex, vertex, }; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::validation::ValidationPolicy; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -474,10 +474,10 @@ Start: Do you need to validate? - A brute-force empty-circumsphere check would be O(simplices × vertices) and is not used by `is_valid()`. In practice, `DelaunayTriangulation::validate()` is usually dominated by Level 3 (topology) work. -As a post-construction acceptance check, the current 8,000-vertex 3D large-scale -debug harness passes `validation_report` for Levels 1–4; on maintainer Apple -M4 Max hardware the final report itself is a low-single-digit-second step, while -the full construction/repair/validation harness is around one minute. +As a post-construction acceptance check, the current 7,500-vertex 3D large-scale +debug harness is the default near-one-minute `validation_report` run for Levels +1–4; on maintainer Apple M4 Max hardware the final report itself is a +low-single-digit-second step. The explicit 10,000-vertex 3D run is a heavier characterization probe that has also passed Levels 1–4 validation, but it is not the default local acceptance helper. @@ -577,9 +577,9 @@ converge, consider the opt-in heuristic rebuild fallback via | 2 | `Tds::validate()` | `tds` | O(N×D²) | | 3 | `Triangulation::is_valid()` | `triangulation` | O(N×D²) | | 3 | `Triangulation::validate()` | `triangulation` | O(N×D²) | -| 4 | `DelaunayTriangulation::is_valid()` | `triangulation::delaunay` | O(simplices) | -| 4 | `DelaunayTriangulation::validate()` | `triangulation::delaunay` | O(simplices × D²) + O(simplices) | -| — | `DelaunayTriangulation::validation_report()` | `triangulation::delaunay` | O(simplices × D²) + O(simplices) | +| 4 | `DelaunayTriangulation::is_valid()` | `delaunay` | O(simplices) | +| 4 | `DelaunayTriangulation::validate()` | `delaunay` | O(simplices × D²) + O(simplices) | +| — | `DelaunayTriangulation::validation_report()` | `delaunay` | O(simplices × D²) + O(simplices) | --- diff --git a/docs/workflows.md b/docs/workflows.md index a9d89de5..ed43ca20 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -14,7 +14,7 @@ For the theoretical background and rationale behind the invariants, see [`invari For most use cases, construction is a single call: ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -39,8 +39,8 @@ Two knobs are commonly used for insertion-time safety vs performance: See [`validation.md`](validation.md) for details. ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, TopologyGuarantee}; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::construction::{DelaunayTriangulation, TopologyGuarantee}; +use delaunay::prelude::validation::ValidationPolicy; let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); @@ -71,9 +71,8 @@ The Builder API is designed to construct Delaunay triangulations, and (by defaul flip-based repair passes during construction. Batch construction uses `ConstructionOptions`, whose default repair cadence is `DelaunayRepairPolicy::EveryInsertion` plus final repair/validation. That cadence reflects the current #341 3D scale acceptance path: the release-mode -`just debug-large-scale-3d 8000 1` harness inserts all 8,000 vertices with zero -skips and finishes final Levels 1–4 validation in the roughly one-minute -maintainer-hardware envelope. The explicit +`just debug-large-scale-3d 7500 1` harness is the current roughly one-minute +maintainer-hardware envelope for final Levels 1–4 validation. The explicit `just debug-large-scale-3d 10000 1` run is a heavier characterization probe that has also passed the same final validation checks. Direct incremental insertion keeps the lower-level `DelaunayRepairPolicy` default at `EveryInsertion`. @@ -83,7 +82,7 @@ The explicit repair methods (`repair_delaunay_with_flips`, `repair_delaunay_with [`numerical_robustness_guide.md`](numerical_robustness_guide.md) for kernel selection guidance. ```rust -use delaunay::prelude::triangulation::construction::{DelaunayRepairPolicy, DelaunayTriangulation}; +use delaunay::prelude::construction::{DelaunayRepairPolicy, DelaunayTriangulation}; let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); @@ -97,7 +96,7 @@ dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); You can also run a global repair pass manually: ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -144,8 +143,8 @@ If repair fails to converge within the flip budget, you get detections, etc.). ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -use delaunay::prelude::triangulation::repair::DelaunayRepairError; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::repair::DelaunayRepairError; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -185,8 +184,8 @@ You can provide explicit seeds for reproducibility; otherwise deterministic defa from the current vertex set. ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -use delaunay::prelude::triangulation::repair::DelaunayRepairHeuristicConfig; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -212,7 +211,7 @@ Toroidal triangulations handle periodic boundary conditions. Use `DelaunayTriangulationBuilder` to construct them: ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulationBuilder, vertex}; +use delaunay::prelude::construction::{DelaunayTriangulationBuilder, vertex}; // 2D periodic triangulation with unit square domain let vertices = vec![ @@ -250,7 +249,7 @@ Data is attached at construction time via `VertexBuilder::data()`, read via the and modified post-construction via `set_vertex_data` / `set_simplex_data`. ```rust -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulationBuilder, Vertex, vertex, }; @@ -306,8 +305,8 @@ If you need observability (or you want to handle skipped vertices explicitly), u `insert_with_statistics()`. ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -use delaunay::prelude::triangulation::insertion::InsertionOutcome; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::insertion::InsertionOutcome; let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); @@ -338,7 +337,7 @@ possible and fan retriangulation otherwise, then runs flip-based Delaunay repair the operation rolls back to the pre-removal triangulation. ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -381,8 +380,8 @@ After using flips, you typically: See [`api_design.md`](api_design.md) for the full Builder vs Edit API design. ```rust -use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -use delaunay::prelude::triangulation::flips::*; +use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +use delaunay::prelude::flips::*; let vertices = vec![ vertex!([0.0, 0.0, 0.0]), diff --git a/examples/delaunayize_repair.rs b/examples/delaunayize_repair.rs index 6e2e3e30..85b7e7be 100644 --- a/examples/delaunayize_repair.rs +++ b/examples/delaunayize_repair.rs @@ -20,10 +20,10 @@ //! cargo run --example delaunayize_repair //! ``` -use delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError; -use delaunay::prelude::triangulation::delaunayize::*; -use delaunay::prelude::triangulation::flips::*; -use delaunay::prelude::triangulation::validation::DelaunayTriangulationValidationError; +use delaunay::prelude::construction::DelaunayTriangulationConstructionError; +use delaunay::prelude::delaunayize::*; +use delaunay::prelude::flips::*; +use delaunay::prelude::validation::DelaunayTriangulationValidationError; // For the generic print_outcome helper. use delaunay::prelude::DataType; diff --git a/examples/diagnostics.rs b/examples/diagnostics.rs index 9114d6c1..c8a112ab 100644 --- a/examples/diagnostics.rs +++ b/examples/diagnostics.rs @@ -11,19 +11,19 @@ #[cfg(feature = "diagnostics")] use delaunay::prelude::DelaunayValidationError; #[cfg(feature = "diagnostics")] +use delaunay::prelude::construction::{ + DelaunayTriangulation, DelaunayTriangulationConstructionError, +}; +#[cfg(feature = "diagnostics")] use delaunay::prelude::diagnostics::{ debug_print_first_delaunay_violation, delaunay_violation_report, }; #[cfg(feature = "diagnostics")] -use delaunay::prelude::geometry::AdaptiveKernel; -#[cfg(feature = "diagnostics")] -use delaunay::prelude::triangulation::construction::{ - DelaunayTriangulation, DelaunayTriangulationConstructionError, -}; +use delaunay::prelude::flips::*; #[cfg(feature = "diagnostics")] -use delaunay::prelude::triangulation::flips::*; +use delaunay::prelude::geometry::AdaptiveKernel; #[cfg(feature = "diagnostics")] -use delaunay::prelude::triangulation::validation::DelaunayTriangulationValidationError; +use delaunay::prelude::validation::DelaunayTriangulationValidationError; #[cfg(feature = "diagnostics")] use delaunay::vertex; diff --git a/examples/numerical_robustness.rs b/examples/numerical_robustness.rs index 0b527d64..b0dfd740 100644 --- a/examples/numerical_robustness.rs +++ b/examples/numerical_robustness.rs @@ -7,14 +7,14 @@ //! It compares kernel behavior on degenerate predicate inputs and shows the //! default adaptive construction path on a small point set. +use delaunay::prelude::construction::{ + DelaunayTriangulation, DelaunayTriangulationConstructionError, +}; use delaunay::prelude::geometry::{ AdaptiveKernel, CircumcenterError, Coordinate, CoordinateConversionError, FastKernel, Kernel, Point, RobustKernel, robust_insphere, robust_orientation, }; -use delaunay::prelude::triangulation::construction::{ - DelaunayTriangulation, DelaunayTriangulationConstructionError, -}; -use delaunay::prelude::triangulation::validation::DelaunayTriangulationValidationError; +use delaunay::prelude::validation::DelaunayTriangulationValidationError; use delaunay::vertex; #[derive(Debug, thiserror::Error)] diff --git a/examples/topology_editing.rs b/examples/topology_editing.rs index 58fc30b1..5ec99c3d 100644 --- a/examples/topology_editing.rs +++ b/examples/topology_editing.rs @@ -23,16 +23,16 @@ reason = "example preserves the crate's typed insertion and flip errors instead of erasing them" )] -use delaunay::prelude::geometry::{ - CircumcenterError, Coordinate, Kernel, Point, circumcenter, hypot, -}; -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulation, DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, }; -use delaunay::prelude::triangulation::flips::*; -use delaunay::prelude::triangulation::insertion::InsertionError; -use delaunay::prelude::triangulation::validation::DelaunayTriangulationValidationError; +use delaunay::prelude::flips::*; +use delaunay::prelude::geometry::{ + CircumcenterError, Coordinate, Kernel, Point, circumcenter, hypot, +}; +use delaunay::prelude::insertion::InsertionError; +use delaunay::prelude::validation::DelaunayTriangulationValidationError; use delaunay::prelude::{TdsError, VertexKey}; type ExampleResult = Result; diff --git a/examples/triangulation_and_hull.rs b/examples/triangulation_and_hull.rs index 33727eb6..e649666d 100644 --- a/examples/triangulation_and_hull.rs +++ b/examples/triangulation_and_hull.rs @@ -14,15 +14,15 @@ use std::num::NonZeroUsize; use std::time::Instant; +use delaunay::prelude::construction::{ + ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationConstructionError, + RetryPolicy, vertex, +}; use delaunay::prelude::generators::{RandomPointGenerationError, generate_random_points_seeded}; use delaunay::prelude::geometry::AdaptiveKernel; use delaunay::prelude::query::{ AdjacencyIndexBuildError, ConvexHull, ConvexHullConstructionError, Coordinate, Point, }; -use delaunay::prelude::triangulation::construction::{ - ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationConstructionError, - RetryPolicy, vertex, -}; type WorkflowTriangulation = DelaunayTriangulation, (), (), D>; diff --git a/justfile b/justfile index 5f5dd22e..54ea39c8 100644 --- a/justfile +++ b/justfile @@ -226,7 +226,7 @@ coverage-ci: _ensure-cargo-llvm-cov debug-large-scale-2d n="36000" repair_every="1": DELAUNAY_BULK_PROGRESS_EVERY=2000 DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS=1800 DELAUNAY_LARGE_DEBUG_N_2D={{ n }} DELAUNAY_LARGE_DEBUG_REPAIR_EVERY={{ repair_every }} cargo test --release --test large_scale_debug debug_large_scale_2d -- --ignored --exact --nocapture -debug-large-scale-3d n="8000" repair_every="1": +debug-large-scale-3d n="7500" repair_every="1": DELAUNAY_BULK_PROGRESS_EVERY=500 DELAUNAY_LARGE_DEBUG_MAX_RUNTIME_SECS=1800 DELAUNAY_LARGE_DEBUG_N_3D={{ n }} DELAUNAY_LARGE_DEBUG_REPAIR_EVERY={{ repair_every }} cargo test --release --test large_scale_debug debug_large_scale_3d -- --ignored --exact --nocapture debug-large-scale-4d n="900" repair_every="1": @@ -274,7 +274,7 @@ help-workflows: @echo "Active large-scale debugging:" @echo " just test-diagnostics # Run diagnostics tools with output" @echo " just debug-large-scale-2d [n] [repair_every] # 2D acceptance/profiling (defaults n=36000, repair_every=1)" - @echo " just debug-large-scale-3d [n] [repair_every] # Issue #341: 3D scalability (defaults n=8000, repair_every=1)" + @echo " just debug-large-scale-3d [n] [repair_every] # Issue #341: 3D scalability (defaults n=7500, repair_every=1)" @echo " just debug-large-scale-4d [n] [repair_every] # Issue #340: 4D large-scale runtime (defaults n=900, repair_every=1)" @echo " just debug-large-scale-5d [n] [repair_every] # Issue #342: 5D feasibility (defaults n=140, repair_every=1)" @echo "" @@ -452,7 +452,7 @@ perf-large-scale-smoke max_secs="60": } run_case "2D" "debug_large_scale_2d" "DELAUNAY_LARGE_DEBUG_N_2D" "36000" "2000" - run_case "3D" "debug_large_scale_3d" "DELAUNAY_LARGE_DEBUG_N_3D" "8000" "500" + run_case "3D" "debug_large_scale_3d" "DELAUNAY_LARGE_DEBUG_N_3D" "7500" "500" run_case "4D" "debug_large_scale_4d" "DELAUNAY_LARGE_DEBUG_N_4D" "900" "100" run_case "5D" "debug_large_scale_5d" "DELAUNAY_LARGE_DEBUG_N_5D" "140" "20" diff --git a/semgrep.yaml b/semgrep.yaml index 17d023dc..f0d1fa06 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -110,7 +110,7 @@ rules: - "/src/core/tds.rs" - "/src/core/triangulation.rs" - "/src/geometry/algorithms/**/*.rs" - - "/src/triangulation/**/*.rs" + - "/src/delaunay/**/*.rs" patterns: - pattern-either: - pattern: use std::collections::HashMap; @@ -168,7 +168,7 @@ rules: - "/src/core/**/*.rs" - "/src/geometry/**/*.rs" - "/src/topology/**/*.rs" - - "/src/triangulation/**/*.rs" + - "/src/delaunay/**/*.rs" - "/src/project_rules/**/*.rs" patterns: - pattern-either: @@ -298,9 +298,9 @@ rules: - "/src/core/**/*.rs" - "/src/geometry/**/*.rs" - "/src/topology/**/*.rs" - - "/src/triangulation/**/*.rs" + - "/src/delaunay/**/*.rs" patterns: - - pattern-regex: '\bcrate::(?:core|geometry|topology|triangulation)::[A-Za-z0-9_]+(?:::[A-Za-z0-9_]+){1,}\b' + - pattern-regex: '\bcrate::(?:core|delaunay|geometry|topology)::[A-Za-z0-9_]+(?:::[A-Za-z0-9_]+){1,}\b' - pattern-inside: | fn $FUNC(...) { ... @@ -554,15 +554,15 @@ rules: - "/benches/**/*.rs" - "/src/project_rules/**/*.rs" patterns: - - pattern-regex: '^\s*use\s+delaunay::(core|geometry|triangulation|topology)::' + - pattern-regex: '^\s*use\s+delaunay::(core|builder|construction|delaunayize|diagnostics|flips|geometry|repair|topology|validation)::' - pattern-not-regex: '^\s*use\s+delaunay::prelude::' - - id: delaunay.rust.prefer-prelude-imports-in-triangulation-doctests + - id: delaunay.rust.prefer-prelude-imports-in-delaunay-doctests languages: - rust severity: WARNING message: >- - Use focused delaunay::prelude imports in public triangulation doctests + Use focused delaunay::prelude imports in public Delaunay doctests instead of deep crate module paths. metadata: category: maintainability @@ -575,7 +575,7 @@ rules: - "/src/**/*.rs" - "/tests/semgrep/src/project_rules/**/*.rs" patterns: - - pattern-regex: '^\s*//[/!]\s*(?:#\s*)?use\s+delaunay::(core|geometry|triangulation|topology)::' + - pattern-regex: '^\s*//[/!]\s*(?:#\s*)?use\s+delaunay::(core|builder|construction|delaunayize|diagnostics|flips|geometry|repair|topology|validation)::' - pattern-not-regex: '^\s*//[/!]\s*(?:#\s*)?use\s+delaunay::prelude::' - id: delaunay.docs.check-before-fix-command-order diff --git a/src/core/adjacency.rs b/src/core/adjacency.rs index 55e37053..1d015d59 100644 --- a/src/core/adjacency.rs +++ b/src/core/adjacency.rs @@ -351,7 +351,7 @@ impl AdjacencyIndex { #[cfg(test)] mod tests { use super::*; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use slotmap::SlotMap; use std::collections::HashSet; diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index 47cb2e37..d08c9e22 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -43,8 +43,9 @@ use crate::core::operations::TopologicalOperation; use crate::core::simplex::{NeighborSlot, Simplex, SimplexValidationError}; use crate::core::tds::{EntityKind, NeighborValidationError, SimplexKey, Tds, VertexKey}; use crate::core::traits::data_type::DataType; -use crate::core::triangulation::{TopologyGuarantee, Triangulation, TriangulationValidationError}; +use crate::core::triangulation::Triangulation; use crate::core::util::stable_hash_u64_slice; +use crate::core::validation::{TopologyGuarantee, TriangulationValidationError}; use crate::core::vertex::Vertex; use crate::geometry::kernel::Kernel; use crate::geometry::point::Point; @@ -59,7 +60,7 @@ use crate::topology::traits::global_topology_model::{ GlobalTopologyModel, GlobalTopologyModelAdapter, }; use crate::topology::traits::topological_space::GlobalTopology; -use crate::triangulation::delaunay::DelaunayTriangulationValidationError; +use crate::validation::DelaunayTriangulationValidationError; use slotmap::Key; use std::borrow::Cow; use std::collections::VecDeque; @@ -81,7 +82,7 @@ type ReplacementPeriodicOffsets = /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::flips::BistellarFlipKind; +/// use delaunay::prelude::flips::BistellarFlipKind; /// /// let kind = BistellarFlipKind::k2(3); /// let inverse = kind.inverse(); @@ -2279,7 +2280,7 @@ where /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::flips::FlipDirection; +/// use delaunay::prelude::flips::FlipDirection; /// /// assert_eq!(FlipDirection::Forward.inverse(), FlipDirection::Inverse); /// ``` @@ -2508,7 +2509,7 @@ impl BistellarFlipKind { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::flips::{BistellarMove, ConstK}; +/// use delaunay::prelude::flips::{BistellarMove, ConstK}; /// /// fn move_k>() -> usize { /// M::K @@ -2524,7 +2525,7 @@ pub struct ConstK; /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::flips::{BistellarMove, ConstK}; +/// use delaunay::prelude::flips::{BistellarMove, ConstK}; /// /// fn move_k>() -> usize { /// M::K @@ -2607,7 +2608,7 @@ impl FlipPredicateError { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::flips::{FlipContextError, FlipError}; +/// use delaunay::prelude::flips::{FlipContextError, FlipError}; /// /// let reason = FlipContextError::ReplacementPeriodicOffsetCountMismatch { /// simplex_count: 2, @@ -3452,7 +3453,7 @@ pub enum FlipVertexAdjacencyError { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::flips::FlipError; +/// use delaunay::prelude::flips::FlipError; /// /// let err = FlipError::UnsupportedDimension { dimension: 1 }; /// assert!(matches!(err, FlipError::UnsupportedDimension { .. })); @@ -3737,7 +3738,7 @@ impl From for FlipFailureKind { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::flips::{BistellarFlipKind, FlipDirection, FlipInfo}; +/// use delaunay::prelude::flips::{BistellarFlipKind, FlipDirection, FlipInfo}; /// use delaunay::prelude::collections::{SimplexKeyBuffer, SmallBuffer, MAX_PRACTICAL_DIMENSION_SIZE}; /// use delaunay::prelude::tds::{SimplexKey, VertexKey}; /// use slotmap::KeyData; @@ -3817,7 +3818,7 @@ pub(crate) struct FlipContextDyn { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::flips::TriangleHandle; +/// use delaunay::prelude::flips::TriangleHandle; /// use delaunay::prelude::tds::VertexKey; /// use slotmap::KeyData; /// @@ -3842,7 +3843,7 @@ impl TriangleHandle { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::flips::TriangleHandle; + /// use delaunay::prelude::flips::TriangleHandle; /// use delaunay::prelude::tds::VertexKey; /// use slotmap::KeyData; /// @@ -3875,7 +3876,7 @@ impl TriangleHandle { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::flips::RidgeHandle; +/// use delaunay::prelude::flips::RidgeHandle; /// use delaunay::prelude::tds::SimplexKey; /// use slotmap::KeyData; /// @@ -3934,7 +3935,7 @@ impl RidgeHandle { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::repair::DelaunayRepairStats; +/// use delaunay::prelude::repair::DelaunayRepairStats; /// /// let stats = DelaunayRepairStats::default(); /// assert_eq!(stats.flips_performed, 0); @@ -4138,7 +4139,7 @@ fn repair_run_from_attempt(outcome: RepairAttemptOutcome) -> DelaunayRepairRun { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::repair::RepairQueueOrder; +/// use delaunay::prelude::repair::RepairQueueOrder; /// /// let order = RepairQueueOrder::Fifo; /// assert_eq!(order, RepairQueueOrder::Fifo); @@ -4156,7 +4157,7 @@ pub enum RepairQueueOrder { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::repair::{ +/// use delaunay::prelude::repair::{ /// DelaunayRepairDiagnostics, RepairQueueOrder, /// }; /// @@ -4226,7 +4227,7 @@ impl fmt::Display for DelaunayRepairDiagnostics { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::repair::{ +/// use delaunay::prelude::repair::{ /// DelaunayRepairError, DelaunayRepairVerificationContext, FlipError, /// }; /// @@ -4292,7 +4293,7 @@ impl fmt::Display for DelaunayRepairVerificationContext { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::repair::{DelaunayRepairError, TopologyGuarantee}; +/// use delaunay::prelude::repair::{DelaunayRepairError, TopologyGuarantee}; /// /// let err = DelaunayRepairError::InvalidTopology { /// required: TopologyGuarantee::PLManifold, @@ -6146,8 +6147,8 @@ where /// # Examples /// /// ``` -/// use delaunay::prelude::triangulation::*; -/// use delaunay::prelude::triangulation::repair::verify_delaunay_via_flip_predicates; +/// use delaunay::prelude::*; +/// use delaunay::prelude::repair::verify_delaunay_via_flip_predicates; /// use delaunay::prelude::geometry::AdaptiveKernel; /// /// let vertices = vec![ @@ -6191,8 +6192,8 @@ where /// # Examples /// /// ``` -/// use delaunay::prelude::triangulation::*; -/// use delaunay::prelude::triangulation::repair::verify_delaunay_for_triangulation; +/// use delaunay::prelude::*; +/// use delaunay::prelude::repair::verify_delaunay_for_triangulation; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -9478,10 +9479,11 @@ mod tests { }; use crate::core::algorithms::locate::LocateResult; use crate::core::collections::Uuid; - use crate::core::triangulation::TopologyGuarantee; + use crate::core::validation::TopologyGuarantee; use crate::geometry::kernel::{AdaptiveKernel, FastKernel}; + use crate::repair::DelaunayRepairOperation; use crate::topology::traits::topological_space::ToroidalConstructionMode; - use crate::triangulation::delaunay::{DelaunayRepairOperation, DelaunayTriangulation}; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use approx::assert_relative_eq; use proptest::prelude::*; diff --git a/src/core/algorithms/incremental_insertion.rs b/src/core/algorithms/incremental_insertion.rs index ad79f15d..829234d0 100644 --- a/src/core/algorithms/incremental_insertion.rs +++ b/src/core/algorithms/incremental_insertion.rs @@ -34,6 +34,7 @@ use crate::core::collections::{ FastHashMap, FastHashSet, FastHasher, MAX_PRACTICAL_DIMENSION_SIZE, SimplexKeyBuffer, SmallBuffer, VertexKeyBuffer, }; +use crate::core::construction::TriangulationConstructionError; use crate::core::facet::{FacetError, FacetHandle}; use crate::core::simplex::{NeighborSlot, Simplex, SimplexValidationError}; use crate::core::tds::{ @@ -42,15 +43,14 @@ use crate::core::tds::{ }; use crate::core::traits::boundary_analysis::BoundaryAnalysis; use crate::core::traits::data_type::DataType; -use crate::core::triangulation::TriangulationConstructionError; -use crate::core::triangulation::TriangulationValidationError; +use crate::core::validation::TriangulationValidationError; use crate::core::vertex::VertexValidationError; use crate::geometry::kernel::Kernel; use crate::geometry::point::Point; use crate::geometry::predicates::Orientation; use crate::geometry::robust_predicates::robust_orientation; use crate::geometry::traits::coordinate::{CoordinateConversionError, CoordinateScalar}; -use crate::triangulation::delaunay::DelaunayTriangulationValidationError; +use crate::validation::DelaunayTriangulationValidationError; use std::fmt; use std::hash::{Hash, Hasher}; @@ -59,7 +59,7 @@ use std::hash::{Hash, Hasher}; /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::insertion::HullExtensionReason; +/// use delaunay::prelude::insertion::HullExtensionReason; /// /// let reason = HullExtensionReason::NoVisibleFacets; /// assert!(matches!(reason, HullExtensionReason::NoVisibleFacets)); @@ -654,7 +654,7 @@ impl From<&DelaunayRepairError> for DelaunayRepairErrorKind { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::repair::{ +/// use delaunay::prelude::repair::{ /// DelaunayRepairError, DelaunayRepairErrorKind, DelaunayRepairErrorSummary, /// }; /// @@ -758,12 +758,12 @@ pub enum InsertionErrorSourceKind { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::insertion::{ +/// use delaunay::prelude::insertion::{ /// DelaunayRepairErrorKind, DelaunayRepairFailureContext, HullExtensionReason, /// InsertionError, InsertionErrorKind, InsertionErrorSourceKind, /// InsertionErrorSummary, /// }; -/// use delaunay::prelude::triangulation::repair::DelaunayRepairError; +/// use delaunay::prelude::repair::DelaunayRepairError; /// /// let source = InsertionError::DelaunayRepairFailed { /// source: Box::new(DelaunayRepairError::PostconditionFailed { @@ -818,7 +818,7 @@ impl InsertionErrorSummary { /// /// ```rust /// use delaunay::prelude::tds::TriangulationValidationErrorKind; - /// use delaunay::prelude::triangulation::insertion::{ + /// use delaunay::prelude::insertion::{ /// InsertionErrorKind, InsertionErrorSourceKind, InsertionErrorSummary, /// }; /// @@ -934,7 +934,7 @@ impl fmt::Display for DelaunayRepairFailureContext { /// /// ```rust /// use delaunay::prelude::tds::SimplexKey; -/// use delaunay::prelude::triangulation::insertion::{ +/// use delaunay::prelude::insertion::{ /// NeighborRebuildError, NeighborWiringError, /// }; /// use slotmap::KeyData; @@ -994,7 +994,7 @@ pub enum NeighborRebuildError { /// /// ```rust /// use delaunay::prelude::tds::SimplexKey; -/// use delaunay::prelude::triangulation::insertion::CavityFillingError; +/// use delaunay::prelude::insertion::CavityFillingError; /// use slotmap::KeyData; /// /// let simplex_key = SimplexKey::from(KeyData::from_ffi(7)); @@ -1134,7 +1134,7 @@ pub enum CavityFillingError { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::insertion::CavityRepairStage; +/// use delaunay::prelude::insertion::CavityRepairStage; /// /// assert_eq!( /// CavityRepairStage::PrimaryInsertion.to_string(), @@ -1165,7 +1165,7 @@ impl fmt::Display for CavityRepairStage { /// /// ```rust /// use delaunay::prelude::tds::SimplexKey; -/// use delaunay::prelude::triangulation::insertion::NeighborWiringError; +/// use delaunay::prelude::insertion::NeighborWiringError; /// use slotmap::KeyData; /// /// let simplex_key = SimplexKey::from(KeyData::from_ffi(11)); @@ -1294,7 +1294,7 @@ pub enum NeighborWiringError { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::insertion::InsertionError; +/// use delaunay::prelude::insertion::InsertionError; /// /// let err = InsertionError::DuplicateCoordinates { /// coordinates: "[0.0, 0.0, 0.0]".to_string(), @@ -1483,7 +1483,7 @@ impl InsertionError { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::insertion::{HullExtensionReason, InsertionError}; + /// use delaunay::prelude::insertion::{HullExtensionReason, InsertionError}; /// /// let retryable = InsertionError::NonManifoldTopology { /// facet_hash: 1, @@ -1700,7 +1700,7 @@ impl InsertionError { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::insertion::fill_cavity; +/// use delaunay::prelude::insertion::fill_cavity; /// use delaunay::prelude::tds::FacetHandle; /// use delaunay::prelude::query::*; /// @@ -2089,7 +2089,7 @@ where /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::insertion::wire_cavity_neighbors; +/// use delaunay::prelude::insertion::wire_cavity_neighbors; /// use delaunay::prelude::collections::SimplexKeyBuffer; /// use delaunay::prelude::tds::Tds; /// @@ -2685,9 +2685,9 @@ where /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::{DelaunayTriangulation, vertex}; -/// use delaunay::prelude::triangulation::DelaunayTriangulationConstructionError; -/// use delaunay::prelude::triangulation::insertion::{ +/// use delaunay::prelude::{DelaunayTriangulation, vertex}; +/// use delaunay::prelude::DelaunayTriangulationConstructionError; +/// use delaunay::prelude::insertion::{ /// InsertionError, TdsMutationError, repair_neighbor_pointers_local, /// }; /// @@ -2936,10 +2936,10 @@ where /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::construction::{ +/// use delaunay::prelude::construction::{ /// DelaunayTriangulation, DelaunayTriangulationConstructionError, vertex, /// }; -/// use delaunay::prelude::triangulation::insertion::{InsertionError, repair_neighbor_pointers}; +/// use delaunay::prelude::insertion::{InsertionError, repair_neighbor_pointers}; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -3332,7 +3332,7 @@ where /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::insertion::extend_hull; +/// use delaunay::prelude::insertion::extend_hull; /// use delaunay::prelude::tds::Tds; /// use delaunay::prelude::tds::VertexKey; /// use delaunay::prelude::geometry::FastKernel; @@ -4188,11 +4188,11 @@ mod tests { use crate::core::algorithms::locate::InternalInconsistencySite; use crate::core::collections::SimplexKeyBuffer; use crate::core::tds::GeometricError; - use crate::core::triangulation::TopologyGuarantee; + use crate::core::validation::TopologyGuarantee; use crate::geometry::kernel::FastKernel; use crate::geometry::traits::coordinate::{Coordinate, CoordinateConversionError}; use crate::topology::characteristics::euler::TopologyClassification; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use slotmap::KeyData; diff --git a/src/core/algorithms/pl_manifold_repair.rs b/src/core/algorithms/pl_manifold_repair.rs index bbc70bde..8821f525 100644 --- a/src/core/algorithms/pl_manifold_repair.rs +++ b/src/core/algorithms/pl_manifold_repair.rs @@ -2,7 +2,7 @@ //! //! This module implements a `pub(crate)` repair algorithm that attempts to bring //! a triangulation closer to satisfying the -//! [`TopologyGuarantee::PLManifold`](crate::core::triangulation::TopologyGuarantee::PLManifold) +//! [`TopologyGuarantee::PLManifold`](crate::core::validation::TopologyGuarantee::PLManifold) //! invariant by removing simplices that cause codimension-1 facet over-sharing //! (facets incident to more than 2 simplices). //! @@ -77,7 +77,7 @@ impl Default for PlManifoldRepairConfig { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::delaunayize::PlManifoldRepairStats; +/// use delaunay::prelude::delaunayize::PlManifoldRepairStats; /// /// let stats = PlManifoldRepairStats::::default(); /// assert_eq!(stats.iterations, 0); @@ -138,7 +138,7 @@ impl Default for PlManifoldRepairStats { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::delaunayize::PlManifoldRepairError; +/// use delaunay::prelude::delaunayize::PlManifoldRepairError; /// /// let err = PlManifoldRepairError::BudgetExhausted { /// iterations: 64, @@ -468,7 +468,7 @@ fn remove_orphaned_vertices( #[cfg(test)] mod tests { use super::*; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; // ============================================================================= diff --git a/src/core/boundary.rs b/src/core/boundary.rs index f3c75cc5..580e7357 100644 --- a/src/core/boundary.rs +++ b/src/core/boundary.rs @@ -258,7 +258,7 @@ mod tests { use crate::core::tds::{SimplexKey, TdsError}; use crate::core::vertex::Vertex; use crate::geometry::{point::Point, traits::coordinate::Coordinate}; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; #[cfg(feature = "bench")] use num_traits::cast::cast; diff --git a/src/core/collections/key_maps.rs b/src/core/collections/key_maps.rs index b0d8e30e..8ceed060 100644 --- a/src/core/collections/key_maps.rs +++ b/src/core/collections/key_maps.rs @@ -38,7 +38,7 @@ pub type VertexUuidSet = FastHashSet; /// /// For key → UUID lookups (less common), use direct topology access: /// ```rust -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ReverseLookupExampleError { @@ -94,7 +94,7 @@ pub type UuidToVertexKeyMap = FastHashMap; /// /// For key → UUID lookups (less common), use direct topology access: /// ```rust -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ReverseLookupExampleError { diff --git a/src/core/collections/secondary_maps.rs b/src/core/collections/secondary_maps.rs index 4b7cb097..99f729ae 100644 --- a/src/core/collections/secondary_maps.rs +++ b/src/core/collections/secondary_maps.rs @@ -31,7 +31,7 @@ use slotmap::SparseSecondaryMap; /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -71,7 +71,7 @@ pub type SimplexSecondaryMap = SparseSecondaryMap; /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), diff --git a/src/core/construction.rs b/src/core/construction.rs new file mode 100644 index 00000000..8b0eb0b6 --- /dev/null +++ b/src/core/construction.rs @@ -0,0 +1,544 @@ +//! Generic triangulation construction helpers. +//! +//! This module owns the generic construction vocabulary for +//! [`Triangulation`](crate::core::triangulation::Triangulation) +//! and the initial-simplex bootstrap used before incremental insertion takes +//! over. Mutation-heavy insertion and repair orchestration remain implemented +//! with the triangulation type until they can be split into narrower modules. + +use crate::core::algorithms::incremental_insertion::{CavityFillingError, HullExtensionReason}; +use crate::core::algorithms::locate::{ConflictError, LocateError}; +use crate::core::collections::{MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer}; +use crate::core::simplex::{Simplex, SimplexValidationError}; +use crate::core::tds::{InvariantErrorSummary, Tds, TdsConstructionError, TdsError, VertexKey}; +use crate::core::traits::data_type::DataType; +use crate::core::triangulation::Triangulation; +use crate::core::validation::TriangulationValidationError; +use crate::core::vertex::Vertex; +use crate::geometry::kernel::Kernel; +use crate::geometry::point::Point; +use crate::geometry::predicates::Orientation; +use crate::geometry::robust_predicates::robust_orientation; +use crate::validation::DelaunayTriangulationValidationError; +use num_traits::NumCast; +use thiserror::Error; + +/// Errors that can occur during triangulation construction. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::triangulation::{ +/// FastKernel, Triangulation, TriangulationConstructionError, vertex, +/// }; +/// +/// let vertices = vec![ +/// vertex!([0.0, 0.0]), +/// vertex!([1.0, 0.0]), +/// vertex!([0.0, 1.0]), +/// ]; +/// let result: Result<_, TriangulationConstructionError> = +/// Triangulation::, (), (), 2>::build_initial_simplex(&vertices); +/// assert!(result.is_ok()); +/// ``` +#[derive(Clone, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum TriangulationConstructionError { + /// Lower-layer construction error in the TDS. + #[error(transparent)] + Tds(#[from] TdsConstructionError), + + /// Failed to create a simplex during triangulation construction. + #[error("Failed to create simplex during construction: {message}")] + FailedToCreateSimplex { + /// Description of the simplex creation failure. + message: String, + }, + + /// Cavity filling failed during incremental construction. + #[error("Cavity filling failed during insertion: {source}")] + InsertionCavityFilling { + /// Underlying cavity-filling error. + #[source] + source: CavityFillingError, + }, + + /// Insufficient vertices to create a triangulation. + #[error("Insufficient vertices for {dimension}D triangulation: {source}")] + InsufficientVertices { + /// The dimension that was attempted. + dimension: usize, + /// The underlying simplex validation error. + source: SimplexValidationError, + }, + + /// Geometric degeneracy prevents triangulation construction. + #[error("Geometric degeneracy encountered during construction: {message}")] + GeometricDegeneracy { + /// Description of the degeneracy issue. + message: String, + }, + + /// Conflict-region extraction failed during incremental construction. + #[error("Conflict region failed during insertion: {source}")] + InsertionConflictRegion { + /// Underlying conflict-region error. + #[source] + source: ConflictError, + }, + + /// Point location failed during incremental construction. + #[error("Point location failed during insertion: {source}")] + InsertionLocation { + /// Underlying point-location error. + #[source] + source: LocateError, + }, + + /// Incremental insertion detected non-manifold topology. + #[error( + "Non-manifold topology during insertion: facet {facet_hash:#x} shared by {simplex_count} simplices" + )] + InsertionNonManifoldTopology { + /// Hash of the over-shared facet. + facet_hash: u64, + /// Number of simplices sharing the facet. + simplex_count: usize, + }, + + /// Hull extension failed during incremental construction. + #[error("Hull extension failed during insertion: {reason}")] + InsertionHullExtension { + /// Structured hull-extension failure reason. + reason: HullExtensionReason, + }, + + /// Level 4 Delaunay validation failed during incremental construction. + #[error("Delaunay validation failed during insertion: {source}")] + InsertionDelaunayValidation { + /// Underlying Delaunay validation error. + #[source] + source: DelaunayTriangulationValidationError, + }, + + /// Level 3 topology validation failed during incremental construction. + #[error("{message}: {source}")] + InsertionTopologyValidation { + /// High-level insertion context. + message: String, + /// Underlying topology validation error. + #[source] + source: TriangulationValidationError, + }, + + /// Final cumulative topology validation failed after construction. + /// + /// Mirrors [`InsertionTopologyValidation`](Self::InsertionTopologyValidation) + /// for post-build checks that run after the incremental insertion phase. + #[error("{message}: {source}")] + FinalTopologyValidation { + /// High-level finalization context. + message: String, + /// Underlying validation error. + #[source] + source: InvariantErrorSummary, + }, + + /// Attempted to insert a vertex with coordinates that already exist. + #[error( + "Duplicate coordinates: vertex with coordinates {coordinates} already exists in the triangulation" + )] + DuplicateCoordinates { + /// String representation of the duplicate coordinates. + coordinates: String, + }, + + /// Internal bookkeeping state became inconsistent during construction. + /// + /// This indicates a bug in the construction algorithm rather than invalid + /// input or geometric degeneracy. + #[error("Internal inconsistency during construction: {message}")] + InternalInconsistency { + /// Description of the inconsistency. + message: String, + }, +} + +impl Triangulation +where + K: Kernel, + K::Scalar: NumCast, + U: DataType, + V: DataType, +{ + /// Build initial D-simplex from D+1 vertices with degeneracy validation. + /// + /// This creates a TDS with a single simplex containing all D+1 vertices, + /// with explicit boundary neighbor slots. The simplex is validated to + /// ensure it is non-degenerate (vertices span full D-dimensional space). + /// + /// **Design Note**: This method uses [`robust_orientation`] directly for + /// the non-degeneracy check, bypassing the kernel. This avoids `SoS` + /// tie-breaking that would mask truly degenerate input and keeps the + /// method independent of kernel state. + /// + /// # Arguments + /// + /// - `vertices`: Exactly D+1 vertices to form the initial simplex + /// + /// # Returns + /// + /// A TDS containing one D-simplex with all vertices, ready for incremental insertion. + /// + /// # Errors + /// + /// Returns an error if the vertex count is not exactly D+1, if the + /// vertices are geometrically degenerate, if vertex/simplex insertion + /// fails, or if duplicate UUIDs are detected. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::triangulation::{FastKernel, Triangulation, vertex}; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let tds = Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; + /// assert_eq!(tds.number_of_vertices(), 3); + /// assert_eq!(tds.number_of_simplices(), 1); + /// assert_eq!(tds.dim(), 2); + /// # Ok::<(), delaunay::prelude::triangulation::TriangulationConstructionError>(()) + /// ``` + pub fn build_initial_simplex( + vertices: &[Vertex], + ) -> Result, TriangulationConstructionError> { + if vertices.len() != D + 1 { + return Err(TriangulationConstructionError::InsufficientVertices { + dimension: D, + source: SimplexValidationError::InsufficientVertices { + actual: vertices.len(), + expected: D + 1, + dimension: D, + }, + }); + } + + for vertex in vertices { + vertex.is_valid().map_err(|source| { + TriangulationConstructionError::Tds(TdsConstructionError::ValidationError( + TdsError::InvalidVertex { + vertex_id: vertex.uuid(), + source, + }, + )) + })?; + } + + let points: SmallBuffer, MAX_PRACTICAL_DIMENSION_SIZE> = + vertices.iter().map(|v| *v.point()).collect(); + + let exact_orientation = robust_orientation(&points[..]).map_err(|e| { + TriangulationConstructionError::FailedToCreateSimplex { + message: format!("Exact orientation test failed: {e}"), + } + })?; + + if matches!(exact_orientation, Orientation::DEGENERATE) { + return Err(TriangulationConstructionError::GeometricDegeneracy { + message: format!( + "Degenerate initial simplex: vertices are collinear/coplanar in {}D space. \ + The {} input vertices do not span a full {}-dimensional simplex. \ + Provide non-degenerate vertices to create a valid triangulation.", + D, + D + 1, + D + ), + }); + } + + let orientation = match exact_orientation { + Orientation::POSITIVE => 1, + Orientation::NEGATIVE => -1, + Orientation::DEGENERATE => { + return Err(TriangulationConstructionError::GeometricDegeneracy { + message: format!("Degenerate initial simplex in {D}D (unreachable)"), + }); + } + }; + + let mut tds = Tds::empty(); + let mut vertex_keys = SmallBuffer::::new(); + for vertex in vertices { + let vkey = tds.insert_vertex_with_mapping(*vertex)?; + vertex_keys.push(vkey); + } + + if orientation < 0 { + if vertex_keys.len() >= 2 { + vertex_keys.swap(0, 1); + } else { + return Err(TriangulationConstructionError::FailedToCreateSimplex { + message: format!( + "Cannot canonicalize orientation for {}D simplex with {} vertex key(s)", + D, + vertex_keys.len(), + ), + }); + } + } + + let simplex = Simplex::new(vertex_keys, None).map_err(|e| { + TriangulationConstructionError::FailedToCreateSimplex { + message: format!("Failed to create initial simplex: {e}"), + } + })?; + + let _simplex_key = tds.insert_simplex_with_mapping(simplex)?; + + tds.assign_neighbors() + .map_err(TdsConstructionError::ValidationError)?; + tds.assign_incident_simplices() + .map_err(|e| TdsConstructionError::ValidationError(e.into()))?; + + Ok(tds) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::simplex::NeighborSlot; + use crate::core::vertex::VertexBuilder; + use crate::geometry::kernel::FastKernel; + use crate::geometry::traits::coordinate::Coordinate; + use crate::vertex; + use uuid::Uuid; + + #[test] + fn internal_inconsistency_display() { + let err = TriangulationConstructionError::InternalInconsistency { + message: "missing vertex in lookup table".to_string(), + }; + + assert_eq!( + err.to_string(), + "Internal inconsistency during construction: missing vertex in lookup table" + ); + } + + macro_rules! test_build_initial_simplex { + ($dim:expr, [$($simplex_coords:expr),+ $(,)?]) => { + pastey::paste! { + #[test] + fn []() { + let vertices: Vec> = vec![ + $(vertex!($simplex_coords)),+ + ]; + + let expected_vertices = vertices.len(); + assert_eq!(expected_vertices, $dim + 1); + + let tds = Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) + .unwrap(); + + assert_eq!(tds.number_of_vertices(), expected_vertices); + assert_eq!(tds.number_of_simplices(), 1); + assert_eq!(tds.dim(), $dim as i32); + assert_eq!(tds.vertices().count(), expected_vertices); + + let (_, simplex) = tds.simplices().next() + .expect("initial simplex should exist"); + assert_eq!(simplex.number_of_vertices(), expected_vertices); + + for (_, vertex) in tds.vertices() { + assert!(vertex.incident_simplex().is_some()); + } + + let neighbors = simplex + .neighbor_slots() + .expect("initial simplex should assign boundary neighbor slots"); + assert!(neighbors.iter().all(|slot| *slot == NeighborSlot::Boundary)); + } + } + }; + } + + test_build_initial_simplex!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); + test_build_initial_simplex!( + 3, + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0] + ] + ); + test_build_initial_simplex!( + 4, + [ + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0] + ] + ); + test_build_initial_simplex!( + 5, + [ + [0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0] + ] + ); + + #[test] + fn build_initial_simplex_insufficient_vertices() { + let vertices = vec![vertex!([0.0, 0.0, 0.0]), vertex!([1.0, 0.0, 0.0])]; + + let result = Triangulation::, (), (), 3>::build_initial_simplex(&vertices); + + assert!(matches!( + result, + Err(TriangulationConstructionError::InsufficientVertices { dimension: 3, .. }) + )); + } + + #[test] + fn build_initial_simplex_too_many_vertices() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([0.5, 0.5]), + ]; + + let result = Triangulation::, (), (), 2>::build_initial_simplex(&vertices); + + assert!(matches!( + result, + Err(TriangulationConstructionError::InsufficientVertices { .. }) + )); + } + + fn invalid_initial_simplex_vertices() -> Vec> { + let mut vertices = Vec::with_capacity(D + 1); + vertices.push(vertex!([0.0_f64; D])); + + let mut invalid_coords = [0.0_f64; D]; + invalid_coords[0] = 1.0; + invalid_coords[1] = f64::NAN; + vertices.push(Vertex::new_with_uuid( + Point::new(invalid_coords), + Uuid::new_v4(), + None, + )); + + for axis in 1..D { + let mut coords = [0.0_f64; D]; + coords[axis] = 1.0; + vertices.push(vertex!(coords)); + } + + vertices + } + + macro_rules! test_build_initial_simplex_rejects_invalid_vertex_dimensions { + ($($dim:expr),+ $(,)?) => { + pastey::paste! { + $( + #[test] + fn []() { + let vertices = invalid_initial_simplex_vertices::<$dim>(); + + let result = Triangulation::, (), (), $dim>::build_initial_simplex(&vertices); + + assert!(matches!( + result, + Err(TriangulationConstructionError::Tds( + TdsConstructionError::ValidationError(TdsError::InvalidVertex { .. }) + )) + )); + } + )+ + } + }; + } + + test_build_initial_simplex_rejects_invalid_vertex_dimensions!(2, 3, 4, 5); + + #[test] + fn build_initial_simplex_with_user_data() { + let v1 = VertexBuilder::default() + .point(Point::new([0.0, 0.0])) + .data(42_usize) + .build() + .unwrap(); + let v2 = VertexBuilder::default() + .point(Point::new([1.0, 0.0])) + .data(43_usize) + .build() + .unwrap(); + let v3 = VertexBuilder::default() + .point(Point::new([0.0, 1.0])) + .data(44_usize) + .build() + .unwrap(); + + let vertices = vec![v1, v2, v3]; + let tds = Triangulation::, usize, (), 2>::build_initial_simplex(&vertices) + .unwrap(); + + assert_eq!(tds.number_of_vertices(), 3); + assert_eq!(tds.number_of_simplices(), 1); + + let data_values: Vec<_> = tds + .vertices() + .filter_map(|(_, v)| v.data.as_ref()) + .copied() + .collect(); + assert_eq!(data_values.len(), 3); + assert!(data_values.contains(&42)); + assert!(data_values.contains(&43)); + assert!(data_values.contains(&44)); + } + + #[test] + fn build_initial_simplex_rejects_collinear_2d() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([2.0, 0.0]), + ]; + + let result = Triangulation::, (), (), 2>::build_initial_simplex(&vertices); + + assert!(matches!( + result, + Err(TriangulationConstructionError::GeometricDegeneracy { .. }) + )); + } + + #[test] + fn build_initial_simplex_rejects_coplanar_3d() { + 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.5, 0.5, 0.0]), + ]; + + let result = Triangulation::, (), (), 3>::build_initial_simplex(&vertices); + + assert!(matches!( + result, + Err(TriangulationConstructionError::GeometricDegeneracy { .. }) + )); + } +} diff --git a/src/core/facet.rs b/src/core/facet.rs index 33cdf5c1..863b3dfc 100644 --- a/src/core/facet.rs +++ b/src/core/facet.rs @@ -34,7 +34,7 @@ //! # Examples //! //! ```rust -//! use delaunay::prelude::triangulation::*; +//! use delaunay::prelude::*; //! use delaunay::prelude::tds::FacetView; //! //! // Create vertices for a tetrahedron @@ -221,7 +221,7 @@ pub enum FacetError { /// # Example /// /// ```rust -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// use delaunay::prelude::tds::{FacetHandle, FacetView}; /// /// let vertices = vec![ @@ -257,7 +257,7 @@ impl FacetHandle { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::FacetHandle; /// /// let vertices = vec![ @@ -284,7 +284,7 @@ impl FacetHandle { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::FacetHandle; /// /// let vertices = vec![ @@ -308,7 +308,7 @@ impl FacetHandle { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::FacetHandle; /// /// let vertices = vec![ @@ -443,7 +443,7 @@ impl<'tds, T, U, V, const D: usize> FacetView<'tds, T, U, V, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::FacetView; /// /// let vertices = vec![ @@ -506,7 +506,7 @@ impl<'tds, T, U, V, const D: usize> FacetView<'tds, T, U, V, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::FacetView; /// /// let vertices = vec![ @@ -563,7 +563,7 @@ impl<'tds, T, U, V, const D: usize> FacetView<'tds, T, U, V, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::FacetView; /// /// let vertices = vec![ @@ -614,7 +614,7 @@ impl<'tds, T, U, V, const D: usize> FacetView<'tds, T, U, V, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::FacetView; /// /// let vertices = vec![ @@ -653,7 +653,7 @@ impl<'tds, T, U, V, const D: usize> FacetView<'tds, T, U, V, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::FacetView; /// /// let vertices = vec![ @@ -740,7 +740,7 @@ impl Eq for FacetView<'_, T, U, V, D> {} /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// use delaunay::prelude::tds::all_facets_for_simplex; /// /// let vertices = vec![ @@ -785,7 +785,7 @@ pub fn all_facets_for_simplex( /// /// ```rust /// use delaunay::prelude::tds::AllFacetsIter; -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -819,7 +819,7 @@ impl<'tds, T, U, V, const D: usize> AllFacetsIter<'tds, T, U, V, D> { /// /// ```rust /// use delaunay::prelude::tds::AllFacetsIter; - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -907,7 +907,7 @@ impl<'tds, T, U, V, const D: usize> Iterator for AllFacetsIter<'tds, T, U, V, D> /// /// ```rust /// use delaunay::prelude::tds::BoundaryFacetsIter; -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -934,7 +934,7 @@ impl<'tds, T, U, V, const D: usize> BoundaryFacetsIter<'tds, T, U, V, D> { /// /// ```rust /// use delaunay::prelude::tds::BoundaryFacetsIter; - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -1063,13 +1063,14 @@ pub fn facet_key_from_vertices(vertices: &[VertexKey]) -> u64 { #[cfg(test)] mod tests { use super::*; + use crate::construction::{ + ConstructionOptions, InitialSimplexStrategy, InsertionOrderStrategy, + }; use crate::core::tds::VertexKey; - use crate::core::triangulation::TopologyGuarantee; + use crate::core::validation::TopologyGuarantee; use crate::core::vertex::Vertex; use crate::geometry::kernel::AdaptiveKernel; - use crate::triangulation::delaunay::{ - ConstructionOptions, DelaunayTriangulation, InitialSimplexStrategy, InsertionOrderStrategy, - }; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use slotmap::SlotMap; use std::{collections::HashSet, mem}; diff --git a/src/core/insertion.rs b/src/core/insertion.rs new file mode 100644 index 00000000..c016ab6c --- /dev/null +++ b/src/core/insertion.rs @@ -0,0 +1,4596 @@ +//! Incremental insertion for generic triangulations. +//! +//! This module owns transactional vertex insertion, duplicate-coordinate +//! detection, perturbation retry, conflict-region shaping, cavity insertion, +//! and insertion telemetry for [`Triangulation`](crate::core::triangulation::Triangulation). + +use crate::core::algorithms::incremental_insertion::{ + CavityFillingError, CavityRepairStage, HullExtensionReason, InsertionError, extend_hull, + external_facets_for_boundary, fill_cavity_replacing_simplices, wire_cavity_neighbors, +}; +#[cfg(debug_assertions)] +use crate::core::algorithms::locate::locate; +#[cfg(feature = "diagnostics")] +use crate::core::algorithms::locate::verify_conflict_region_completeness; +use crate::core::algorithms::locate::{ + ConflictError, LocateError, LocateResult, LocateStats, extract_cavity_boundary, + find_conflict_region, locate_by_scan, locate_with_stats, locate_with_trace, +}; +use crate::core::collections::spatial_hash_grid::HashGridIndex; +use crate::core::collections::{CavityBoundaryBuffer, SimplexKeyBuffer}; +use crate::core::facet::FacetHandle; +use crate::core::operations::{ + InsertionOutcome, InsertionResult, InsertionStatistics, InsertionTelemetry, + InsertionTelemetryMode, SuspicionFlags, +}; +#[cfg(debug_assertions)] +use crate::core::simplex::Simplex; +use crate::core::tds::{InvariantError, SimplexKey, Tds, TdsError, VertexKey}; +use crate::core::traits::data_type::DataType; +use crate::core::triangulation::Triangulation; +use crate::core::vertex::Vertex; +use crate::geometry::kernel::Kernel; +use crate::geometry::point::Point; +use crate::geometry::traits::coordinate::{Coordinate, CoordinateScalar}; +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 num_traits::{Float, NumCast, One, Zero}; +use std::borrow::Cow; +use std::env; +use std::fmt::Write as _; +use std::sync::{ + OnceLock, + atomic::{AtomicBool, AtomicU64, Ordering}, +}; +use std::time::{Duration, Instant}; +use uuid::Uuid; + +/// Maximum number of repair iterations for fixing non-manifold topology after insertion. +/// +/// This limit prevents infinite loops in the rare case where repair cannot make progress. +/// In practice, most insertions require 0-2 iterations to restore manifold topology. +const MAX_REPAIR_ITERATIONS: usize = 10; + +/// Default number of perturbation retries for transactional insertion. +/// +/// Each retry uses a progressively larger perturbation magnitude (×10 per attempt), +/// so 3 retries span 4 orders of magnitude (e.g. `1e-8` → `1e-5` × `local_scale` for f64). +const DEFAULT_PERTURBATION_RETRIES: usize = 3; + +/// Telemetry: counts how often the topology safety-net recovered from a Level 3 validation +/// failure by retrying insertion with a star-split of the containing simplex. +/// +/// This is a process-wide counter across all triangulation instances. +/// +/// This counter is intentionally lightweight and can be polled by production workloads +/// to see whether this recovery path is frequently used. +static TOPOLOGY_SAFETY_NET_STAR_SPLIT_FALLBACK_SUCCESSES: AtomicU64 = AtomicU64::new(0); +static DUPLICATE_DETECTION_TOTAL: AtomicU64 = AtomicU64::new(0); +static DUPLICATE_DETECTION_GRID_USED: AtomicU64 = AtomicU64::new(0); +static DUPLICATE_DETECTION_GRID_FALLBACKS: AtomicU64 = AtomicU64::new(0); +static DUPLICATE_DETECTION_GRID_CANDIDATES: AtomicU64 = AtomicU64::new(0); +static DUPLICATE_DETECTION_ENABLED: OnceLock = OnceLock::new(); +static RETRYABLE_SKIP_TRACE_ENABLED: OnceLock = OnceLock::new(); +static CAVITY_REDUCTION_TRACE_ENABLED: OnceLock = OnceLock::new(); +static CAVITY_REDUCTION_TRACE_EMITTED: AtomicBool = AtomicBool::new(false); + +fn duplicate_detection_metrics_enabled() -> bool { + #[cfg(test)] + if tests::duplicate_detection_force_enabled() { + return true; + } + *DUPLICATE_DETECTION_ENABLED.get_or_init(|| env::var_os("DELAUNAY_DUPLICATE_METRICS").is_some()) +} + +/// Caches whether retryable conflict-region skips should emit release-visible traces. +fn retryable_skip_trace_enabled() -> bool { + *RETRYABLE_SKIP_TRACE_ENABLED + .get_or_init(|| env::var_os("DELAUNAY_DEBUG_RETRYABLE_SKIP").is_some()) +} + +/// Returns whether the first cavity-reduction chain should emit release-visible tracing. +fn cavity_reduction_trace_enabled() -> bool { + *CAVITY_REDUCTION_TRACE_ENABLED + .get_or_init(|| env::var_os("DELAUNAY_DEBUG_CAVITY_REDUCTION_ONCE").is_some()) +} + +/// Extracts a compact one-line summary for retryable conflict-region failures. +/// +/// These summaries are designed for the large-scale debug harness logs, where we want +/// enough structure to correlate repeated ridge-fan failures without dumping the entire +/// conflict region. +fn retryable_conflict_trace_detail(error: &InsertionError) -> Option { + match error { + InsertionError::ConflictRegion(ConflictError::NonManifoldFacet { + facet_hash, + simplex_count, + }) => Some(format!( + "kind=non_manifold_facet facet_hash={facet_hash:#x} simplex_count={simplex_count}" + )), + InsertionError::ConflictRegion(ConflictError::RidgeFan { + facet_count, + ridge_vertex_count, + extra_simplices, + }) => Some(format!( + "kind=ridge_fan facet_count={facet_count} ridge_vertex_count={ridge_vertex_count} \ + extra_simplices={}", + extra_simplices.len() + )), + InsertionError::ConflictRegion(ConflictError::DisconnectedBoundary { + visited, + total, + disconnected_simplices, + }) => Some(format!( + "kind=disconnected_boundary visited={visited} total={total} disconnected_simplices={}", + disconnected_simplices.len() + )), + InsertionError::ConflictRegion(ConflictError::OpenBoundary { + facet_count, + ridge_vertex_count, + .. + }) => Some(format!( + "kind=open_boundary facet_count={facet_count} ridge_vertex_count={ridge_vertex_count}" + )), + _ => None, + } +} + +/// Formats a compact summary for cavity-boundary extraction failures. +fn cavity_conflict_error_summary(error: &ConflictError) -> String { + match error { + ConflictError::NonManifoldFacet { + facet_hash, + simplex_count, + } => format!("non_manifold_facet facet_hash={facet_hash:#x} simplex_count={simplex_count}"), + ConflictError::RidgeFan { + facet_count, + ridge_vertex_count, + extra_simplices, + } => format!( + "ridge_fan facet_count={facet_count} ridge_vertex_count={ridge_vertex_count} \ + extra_simplices={}", + extra_simplices.len() + ), + ConflictError::DisconnectedBoundary { + visited, + total, + disconnected_simplices, + } => format!( + "disconnected_boundary visited={visited} total={total} disconnected_simplices={}", + disconnected_simplices.len() + ), + ConflictError::OpenBoundary { + facet_count, + ridge_vertex_count, + open_simplex, + } => format!( + "open_boundary facet_count={facet_count} ridge_vertex_count={ridge_vertex_count} \ + open_simplex={open_simplex:?}" + ), + ConflictError::InvalidStartSimplex { simplex_key } => { + format!("invalid_start_simplex simplex_key={simplex_key:?}") + } + ConflictError::PredicateError { source } => { + format!("predicate_error source={source}") + } + ConflictError::SimplexDataAccessFailed { + simplex_key, + message, + } => { + format!("simplex_data_access_failed simplex_key={simplex_key:?} message={message}") + } + ConflictError::InternalInconsistency { site } => { + format!("internal_inconsistency site={site}") + } + } +} + +/// Emits one-shot tracing for the first cavity-reduction chain in a run. +/// +/// Routed through `tracing::debug!`; enable with `RUST_LOG=debug` (the +/// large-scale debug harness wires this up automatically when +/// `DELAUNAY_DEBUG_CAVITY_REDUCTION_ONCE` is set). +fn log_cavity_reduction_event( + enabled: bool, + iteration: usize, + conflict_simplices: &SimplexKeyBuffer, + event: F, +) where + F: FnOnce() -> String, +{ + if !enabled { + return; + } + + let conflict_preview: Vec = conflict_simplices.iter().copied().take(12).collect(); + let event = event(); + tracing::debug!( + target: "delaunay::cavity_reduction", + iteration, + conflict_simplices = conflict_simplices.len(), + event, + conflict_preview = ?conflict_preview, + "cavity-reduction event" + ); +} + +#[expect( + clippy::too_many_arguments, + reason = "Diagnostic helper keeps retryable skip instrumentation centralized" +)] +/// Emits a single structured line for a retryable conflict-region skip after rollback. +/// +/// Logging after rollback lets the trace report both the state we tried to modify and +/// the restored simplex/vertex counts that future attempts will see. Routed through +/// `tracing::debug!` so callers can filter it via `RUST_LOG`; enabled for release-mode +/// runs by `DELAUNAY_DEBUG_RETRYABLE_SKIP`. +fn log_retryable_conflict_skip( + bulk_index: Option, + uuid: Uuid, + attempt: usize, + max_attempts: usize, + used_perturbation: bool, + will_retry: bool, + simplices_before_attempt: usize, + vertices_before_attempt: usize, + simplices_after_rollback: usize, + vertices_after_rollback: usize, + detail: &str, + error: &InsertionError, +) { + if !retryable_skip_trace_enabled() { + return; + } + + let bulk_index_display = bulk_index.map_or_else(|| String::from("n/a"), |idx| idx.to_string()); + tracing::debug!( + target: "delaunay::retryable_skip", + bulk_index = %bulk_index_display, + uuid = %uuid, + attempt, + max_attempts, + used_perturbation, + rolled_back = true, + will_retry, + simplices_before_attempt, + vertices_before_attempt, + simplices_after_rollback, + vertices_after_rollback, + conflict = %detail, + error = %error, + "retryable conflict-region skip after rollback" + ); +} + +/// Telemetry counters for duplicate-coordinate detection. +#[must_use] +#[derive(Debug, Clone, Copy, Default)] +pub struct DuplicateDetectionMetrics { + /// Total number of duplicate-coordinate checks executed. + pub total_checks: u64, + /// Number of checks that successfully used the hash grid. + pub grid_used: u64, + /// Number of checks that fell back to a non-grid scan. + pub grid_fallbacks: u64, + /// Total candidate vertices inspected during grid-based checks. + pub grid_candidates: u64, +} + +pub(crate) fn record_duplicate_detection_metrics( + used_grid: bool, + candidate_count: usize, + fell_back: bool, +) { + if !duplicate_detection_metrics_enabled() { + return; + } + DUPLICATE_DETECTION_TOTAL.fetch_add(1, Ordering::Relaxed); + if used_grid { + DUPLICATE_DETECTION_GRID_USED.fetch_add(1, Ordering::Relaxed); + DUPLICATE_DETECTION_GRID_CANDIDATES.fetch_add(candidate_count as u64, Ordering::Relaxed); + } + if fell_back { + DUPLICATE_DETECTION_GRID_FALLBACKS.fetch_add(1, Ordering::Relaxed); + } +} + +struct TryInsertImplOk { + /// Inserted vertex key plus an optional locate hint for the caller. + inserted: (VertexKey, Option), + /// Number of simplices removed during local non-manifold repair. + simplices_removed: usize, + /// Suspicion flags observed during the insertion attempt. + suspicion: SuspicionFlags, + /// Simplices touched by insertion that should seed follow-up local repair. + /// + /// This includes live simplices created by the insertion plus simplices that were shrunk + /// out of the final conflict region so higher layers can revisit nearby + /// Delaunay violations without rediscovering the inserted vertex star globally. + repair_seed_simplices: SimplexKeyBuffer, + /// Whether the insertion path can leave local Delaunay work for the caller. + /// + /// Clean interior Bowyer-Watson insertions preserve the Delaunay property. + /// Exterior hull extensions and suspicious fallback/repair paths still need + /// a local flip-repair pass. + delaunay_repair_required: bool, +} + +/// Result of filling one insertion cavity, including the follow-up Delaunay +/// repair requirements that depend on how the cavity was shaped. +struct CavityInsertionOutcome { + /// Locate hint for the next insertion. + hint: Option, + /// Number of simplices removed during local non-manifold repair. + simplices_removed: usize, + /// Simplices touched by insertion that should seed follow-up local repair. + repair_seed_simplices: SimplexKeyBuffer, + /// Whether this cavity path can leave Delaunay work for the caller. + delaunay_repair_required: bool, +} + +enum InsertionSite<'a> { + Interior { + start_simplex: SimplexKey, + conflict_simplices: Cow<'a, SimplexKeyBuffer>, + }, + Exterior { + conflict_simplices: Option>, + repair_seed_simplices: SimplexKeyBuffer, + }, +} + +/// Internal insertion result that preserves the user-facing outcome plus +/// hidden repair seeding used by batch/debug construction paths. +pub(crate) struct DetailedInsertionResult { + /// Public insertion outcome returned to higher layers. + pub outcome: InsertionOutcome, + /// Public statistics collected while attempting the insertion. + pub stats: InsertionStatistics, + /// Internal path telemetry collected while attempting the insertion. + pub telemetry: InsertionTelemetry, + /// Local simplices that should seed the caller's Delaunay repair set. + pub repair_seed_simplices: SimplexKeyBuffer, + /// Whether callers should run Delaunay repair over `repair_seed_simplices`. + pub delaunay_repair_required: bool, +} + +// ============================================================================= +// Geometric Operations (Requires Extra Numeric Conversion Bounds) +// ============================================================================= + +impl Triangulation +where + K: Kernel, + K::Scalar: NumCast, + U: DataType, + V: DataType, +{ + /// Returns the number of times the topology safety-net recovered from a Level 3 + /// validation failure by retrying insertion with a star-split of the containing simplex. + /// + /// This is a process-wide counter (across all triangulation instances) intended for + /// production telemetry. A high value suggests the cavity-based insertion frequently + /// creates transient invalid topology that is being masked by the fallback. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::geometry::FastKernel; + /// use delaunay::prelude::Triangulation; + /// + /// let count = Triangulation::, (), (), 3> + /// ::topology_safety_net_star_split_fallback_successes(); + /// assert!(count >= 0); + /// ``` + #[must_use] + pub fn topology_safety_net_star_split_fallback_successes() -> u64 { + TOPOLOGY_SAFETY_NET_STAR_SPLIT_FALLBACK_SUCCESSES.load(Ordering::Relaxed) + } + + /// Returns duplicate-detection telemetry if enabled via `DELAUNAY_DUPLICATE_METRICS`. + /// + /// This is a process-wide counter (across all triangulation instances). It reports how often + /// duplicate checks used the hash grid versus falling back to linear scans, along with the + /// total candidate count inspected during grid queries. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::geometry::FastKernel; + /// use delaunay::prelude::{DuplicateDetectionMetrics, Triangulation}; + /// + /// let metrics = Triangulation::, (), (), 3> + /// ::duplicate_detection_metrics(); + /// let _ = metrics; // None unless DELAUNAY_DUPLICATE_METRICS is set + /// ``` + #[must_use] + pub fn duplicate_detection_metrics() -> Option { + if !duplicate_detection_metrics_enabled() { + return None; + } + Some(DuplicateDetectionMetrics { + total_checks: DUPLICATE_DETECTION_TOTAL.load(Ordering::Relaxed), + grid_used: DUPLICATE_DETECTION_GRID_USED.load(Ordering::Relaxed), + grid_fallbacks: DUPLICATE_DETECTION_GRID_FALLBACKS.load(Ordering::Relaxed), + grid_candidates: DUPLICATE_DETECTION_GRID_CANDIDATES.load(Ordering::Relaxed), + }) + } + + /// Insert a vertex with statistics, using a custom perturbation seed and an optional + /// spatial hash-grid index, and also return the simplices that cavity reduction touched + /// and left in place. + /// + /// The extra seed set stays internal so bulk construction and debug rebuilds can widen + /// their local repair frontier without changing the public insertion API. + pub(crate) fn insert_with_statistics_seeded_indexed_detailed( + &mut self, + vertex: Vertex, + conflict_simplices: Option<&SimplexKeyBuffer>, + hint: Option, + perturbation_seed: u64, + index: Option<&mut HashGridIndex>, + bulk_index: Option, + ) -> Result { + self.insert_with_statistics_seeded_indexed_detailed_with_telemetry( + vertex, + conflict_simplices, + hint, + perturbation_seed, + index, + bulk_index, + InsertionTelemetryMode::CountsOnly, + ) + } + + /// Insert a vertex with statistics and explicitly selected telemetry collection. + /// + /// Use [`InsertionTelemetryMode::CountsAndTimings`] only when the caller will + /// consume elapsed-time telemetry; the default detailed insertion path records + /// counters without paying per-attempt `Instant::now()` costs. + #[expect( + clippy::too_many_arguments, + reason = "Internal detailed insertion carries perturbation, spatial-index, trace, and telemetry knobs" + )] + pub(crate) fn insert_with_statistics_seeded_indexed_detailed_with_telemetry( + &mut self, + vertex: Vertex, + conflict_simplices: Option<&SimplexKeyBuffer>, + hint: Option, + perturbation_seed: u64, + index: Option<&mut HashGridIndex>, + bulk_index: Option, + telemetry_mode: InsertionTelemetryMode, + ) -> Result { + self.insert_transactional_detailed( + vertex, + conflict_simplices, + hint, + DEFAULT_PERTURBATION_RETRIES, + perturbation_seed, + index, + bulk_index, + telemetry_mode, + ) + } + + /// Transactional insertion with automatic rollback and perturbation retry, plus + /// the local-repair seed simplices discovered while shaping the cavity. + #[expect( + clippy::too_many_lines, + reason = "Complex insertion logic; splitting further would harm readability" + )] + #[expect( + clippy::too_many_arguments, + reason = "Transactional insertion needs the bulk-index diagnostic context for #204 tracing" + )] + fn insert_transactional_detailed( + &mut self, + vertex: Vertex, + conflict_simplices: Option<&SimplexKeyBuffer>, + hint: Option, + max_perturbation_attempts: usize, + perturbation_seed: u64, + mut index: Option<&mut HashGridIndex>, + bulk_index: Option, + telemetry_mode: InsertionTelemetryMode, + ) -> Result { + let mut stats = InsertionStatistics::default(); + let mut telemetry = InsertionTelemetry::default(); + let original_coords = *vertex.point().coords(); + let original_uuid = vertex.uuid(); + let mut current_vertex = vertex; + // Preserve the last retryable failure so an exhausted perturbation loop can + // explain why the vertex was skipped instead of reporting a generic error. + let mut last_retryable_error: Option = None; + + // Reuse the caller's spatial index as a locate-hint source when batch insertion did + // not already provide a better hint. This keeps retries and bulk runs on the same + // point-location path. + let mut hint = hint; + if hint.is_none() + && let Some(index_ref) = index.as_deref() + { + hint = self.select_locate_hint_from_hash_grid(&original_coords, index_ref); + } + + // Scale perturbations against the local neighborhood so retries stay small relative + // to the nearby geometry instead of using a single global epsilon. + let local_scale = self.estimate_local_perturbation_scale(&original_coords, hint); + + let duplicate_tolerance = + self.estimate_duplicate_coordinate_tolerance(&original_coords, hint); + self.ensure_duplicate_index_cell_size(index.as_deref_mut(), duplicate_tolerance); + + // Base perturbation epsilon: ≈ √machine_epsilon for the scalar type. + let epsilon_value: f64 = if K::Scalar::mantissa_digits() <= 24 { + 1e-4 + } else { + 1e-8 + }; + + for attempt in 0..=max_perturbation_attempts { + stats.attempts = attempt + 1; + + // Attempt 0 uses the caller's coordinates verbatim; later attempts apply a + // deterministic signed perturbation so the same seed reproduces the same path. + if attempt > 0 { + let mut perturbed_coords = original_coords; + // Progressive local-scale perturbation: magnitude grows ×10 per attempt. + // attempt 1: base × local_scale × 10 + // attempt 2: base × local_scale × 100 + // attempt 3: base × local_scale × 1000 + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "attempt is at most DEFAULT_PERTURBATION_RETRIES (3), fits in i32" + )] + let scale_factor = 10.0_f64.powi(attempt as i32); + let Some(epsilon) = ::from(epsilon_value * scale_factor) + else { + // We failed to convert the perturbation scale into the scalar type. + // + // This should not happen for our supported scalar types (`f32`, `f64`), but if it + // does (e.g. with a custom scalar), we degrade gracefully by skipping this vertex + // rather than aborting the whole insertion. + stats.result = InsertionResult::SkippedDegeneracy; + let error = last_retryable_error.unwrap_or_else(|| { + CavityFillingError::PerturbationScaleConversion { + value: epsilon_value.to_string(), + } + .into() + }); + return Ok(DetailedInsertionResult { + outcome: InsertionOutcome::Skipped { error }, + stats, + telemetry, + repair_seed_simplices: SimplexKeyBuffer::new(), + delaunay_repair_required: false, + }); + }; + + let perturbation_scale = epsilon * local_scale; + for (idx, coord) in perturbed_coords.iter_mut().enumerate() { + let coord_scale = + ::from(idx + 1).unwrap_or_else(K::Scalar::one); + let signed_perturbation = if perturbation_seed == 0 { + if (attempt + idx) % 2 == 0 { + perturbation_scale + } else { + -perturbation_scale + } + } else { + let mix = perturbation_seed + ^ ((attempt as u64) << 32) + ^ (idx as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15); + if mix & 1 == 0 { + perturbation_scale + } else { + -perturbation_scale + } + }; + *coord += signed_perturbation * coord_scale; + } + + // Preserve the caller-provided vertex UUID across perturbation retries. + // This ensures the inserted vertex retains its original identity even if we have + // to retry with perturbed coordinates. + current_vertex = + Vertex::new_with_uuid(Point::new(perturbed_coords), original_uuid, vertex.data); + } + + // Duplicate coordinate detection uses the hash grid when available; otherwise it + // falls back to a linear scan (O(n·D) per insertion, O(n²·D) worst-case). + if let Some(error) = self.duplicate_coordinates_error( + current_vertex.point().coords(), + duplicate_tolerance, + index.as_deref(), + ) { + stats.result = InsertionResult::SkippedDuplicate; + #[cfg(debug_assertions)] + tracing::debug!("SKIPPED: {error}"); + return Ok(DetailedInsertionResult { + outcome: InsertionOutcome::Skipped { error }, + stats, + telemetry, + repair_seed_simplices: SimplexKeyBuffer::new(), + delaunay_repair_required: false, + }); + } + + let simplices_before_attempt = self.tds.number_of_simplices(); + let vertices_before_attempt = self.tds.number_of_vertices(); + + // Clone TDS for rollback (transactional semantics) + let tds_snapshot = self.tds.clone_for_rollback(); + + // Try insertion. + // + // Topology safety net: ensure we don't commit an insertion that breaks Level 3 topology. + // If the cavity-based insertion produces an Euler/topology mismatch, roll back and retry a + // conservative fallback (star-split of the containing simplex) within the same transactional attempt. + #[cfg(test)] + // Test-only hook for deterministic coverage of the rollback + perturbation retry + // success path, which is otherwise rare under the adaptive SoS predicates. + let result = if tests::take_force_next_insertion_retryable_failure() { + Err(InsertionError::NonManifoldTopology { + facet_hash: 0x000F_0CED, + simplex_count: 3, + }) + } else { + self.try_insert_with_topology_safety_net( + current_vertex, + conflict_simplices, + hint, + attempt, + &tds_snapshot, + &mut telemetry, + telemetry_mode, + ) + }; + #[cfg(not(test))] + let result = self.try_insert_with_topology_safety_net( + current_vertex, + conflict_simplices, + hint, + attempt, + &tds_snapshot, + &mut telemetry, + telemetry_mode, + ); + + match result { + Ok(TryInsertImplOk { + inserted, + simplices_removed, + repair_seed_simplices, + delaunay_repair_required, + .. + }) => { + stats.simplices_removed_during_repair = simplices_removed; + stats.result = InsertionResult::Inserted; + #[cfg(debug_assertions)] + if attempt > 0 { + tracing::debug!( + "Warning: Geometric degeneracy resolved via perturbation (attempt {attempt})" + ); + } + + let (vertex_key, hint) = inserted; + // Only the committed attempt updates the duplicate index. Earlier + // retries all rolled back to the pre-attempt triangulation state. + if let Some(index) = index.as_deref_mut() + && let Some(vertex) = self.tds.vertex(vertex_key) + { + index.insert_vertex(vertex_key, vertex.point().coords()); + } + + return Ok(DetailedInsertionResult { + outcome: InsertionOutcome::Inserted { vertex_key, hint }, + stats, + telemetry, + repair_seed_simplices, + delaunay_repair_required, + }); + } + Err(e) => { + // Any error - rollback to snapshot + self.tds = tds_snapshot; + + // Handle duplicate coordinates specially - skip immediately without retry + if matches!(e, InsertionError::DuplicateCoordinates { .. }) { + stats.result = InsertionResult::SkippedDuplicate; + #[cfg(debug_assertions)] + tracing::debug!("SKIPPED: {e}"); + return Ok(DetailedInsertionResult { + outcome: InsertionOutcome::Skipped { error: e }, + stats, + telemetry, + repair_seed_simplices: SimplexKeyBuffer::new(), + delaunay_repair_required: false, + }); + } + + // Check if this is a retryable error (geometric degeneracy) + let is_retryable = e.is_retryable(); + + // Emit the conflict summary after rollback so the trace captures the + // restored manifold state that the next retry will start from. + if retryable_skip_trace_enabled() + && let Some(detail) = retryable_conflict_trace_detail(&e) + { + log_retryable_conflict_skip( + bulk_index, + original_uuid, + attempt + 1, + max_perturbation_attempts + 1, + attempt > 0, + is_retryable && attempt < max_perturbation_attempts, + simplices_before_attempt, + vertices_before_attempt, + self.tds.number_of_simplices(), + self.tds.number_of_vertices(), + &detail, + &e, + ); + } + + if is_retryable && attempt < max_perturbation_attempts { + last_retryable_error = Some(e.clone()); + #[cfg(debug_assertions)] + tracing::debug!( + "RETRYING: Attempt {} failed with: {e}. Applying perturbation...", + attempt + 1 + ); + } else if is_retryable { + stats.result = InsertionResult::SkippedDegeneracy; + #[cfg(debug_assertions)] + tracing::debug!( + "SKIPPED: Could not insert vertex after {} attempts (max perturbation ≈ {:.0e} × local_scale). Last error: {e}. Vertex skipped to maintain manifold.", + max_perturbation_attempts + 1, + epsilon_value + * 10.0_f64.powi( + #[expect( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + reason = "max_perturbation_attempts is small, fits in i32" + )] + { + max_perturbation_attempts as i32 + } + ), + ); + return Ok(DetailedInsertionResult { + outcome: InsertionOutcome::Skipped { error: e }, + stats, + telemetry, + // Skipped insertions do not mutate the triangulation, so any + // intermediate cavity-seed hints are irrelevant to callers. + repair_seed_simplices: SimplexKeyBuffer::new(), + delaunay_repair_required: false, + }); + } else { + // Non-retryable structural error (e.g., duplicate UUID) + return Err(e); + } + } + } + } + + Err(InsertionError::TopologyValidation( + TdsError::InconsistentDataStructure { + message: "insertion retry loop exhausted without producing an outcome".to_string(), + }, + )) + } + + fn select_locate_hint_from_hash_grid( + &self, + coords: &[K::Scalar; D], + index: &HashGridIndex, + ) -> Option { + let mut best: Option<(K::Scalar, SimplexKey)> = None; + + index.for_each_candidate_vertex_key(coords, |vkey| { + let Some(vertex) = self.tds.vertex(vkey) else { + return true; + }; + + let Some(simplex_key) = vertex.incident_simplex() else { + return true; + }; + + if !self.tds.contains_simplex(simplex_key) { + return true; + } + + let vcoords = vertex.point().coords(); + let mut dist_sq = K::Scalar::zero(); + for i in 0..D { + let diff = vcoords[i] - coords[i]; + dist_sq += diff * diff; + } + + match best { + Some((best_dist, _)) if dist_sq >= best_dist => {} + _ => { + best = Some((dist_sq, simplex_key)); + } + } + + true + }); + + best.map(|(_, simplex_key)| simplex_key) + } + + /// Chooses the relative duplicate-coordinate tolerance for the scalar precision. + fn duplicate_relative_tolerance() -> K::Scalar { + let value = if K::Scalar::mantissa_digits() <= 24 { + 1e-6_f64 + } else { + 1e-10_f64 + }; + ::from(value).unwrap_or_else(K::Scalar::default_tolerance) + } + + /// Keeps duplicate-scale estimates tied to existing geometry rather than + /// hard-coding a scalar-unit epsilon. + fn include_duplicate_scale_reference( + point_coords: &[K::Scalar; D], + axis_min: &mut [K::Scalar; D], + axis_max: &mut [K::Scalar; D], + magnitude_scale: &mut K::Scalar, + saw_reference: &mut bool, + ) { + *saw_reference = true; + for i in 0..D { + let coord = point_coords[i]; + if coord < axis_min[i] { + axis_min[i] = coord; + } + if coord > axis_max[i] { + axis_max[i] = coord; + } + + let abs = coord.abs(); + if abs > *magnitude_scale { + *magnitude_scale = abs; + } + } + } + + /// Estimates a duplicate-coordinate tolerance from the local simplex span plus + /// a small ULP-scaled floor for translated coordinate systems. + fn estimate_duplicate_coordinate_tolerance( + &self, + coords: &[K::Scalar; D], + hint: Option, + ) -> K::Scalar { + let mut axis_min = *coords; + let mut axis_max = *coords; + let mut magnitude_scale = K::Scalar::zero(); + let mut saw_reference = false; + let mut local_feature_scale = None; + + for coord in coords { + let abs = (*coord).abs(); + if abs > magnitude_scale { + magnitude_scale = abs; + } + } + + if let Some(simplex_key) = hint + && let Some(simplex) = self.tds.simplex(simplex_key) + { + for &vkey in simplex.vertices() { + if let Some(vertex) = self.tds.vertex(vkey) { + Self::include_duplicate_scale_reference( + vertex.point().coords(), + &mut axis_min, + &mut axis_max, + &mut magnitude_scale, + &mut saw_reference, + ); + } + } + } + + if !saw_reference { + let local_scale = self.estimate_local_perturbation_scale(coords, None); + if local_scale.is_finite() && local_scale > K::Scalar::zero() { + if local_scale > magnitude_scale { + magnitude_scale = local_scale; + } + local_feature_scale = Some(local_scale); + } + } + + let feature_scale = local_feature_scale.unwrap_or_else(|| { + let mut span_sq = K::Scalar::zero(); + for i in 0..D { + let span = axis_max[i] - axis_min[i]; + span_sq += span * span; + } + span_sq.sqrt() + }); + let relative_tolerance = Self::duplicate_relative_tolerance() * feature_scale; + let ulp_factor = ::from(16.0_f64).unwrap_or_else(K::Scalar::one); + let ulp_tolerance = K::Scalar::epsilon() * ulp_factor * magnitude_scale; + let mut tolerance = if relative_tolerance > ulp_tolerance { + relative_tolerance + } else { + ulp_tolerance + }; + + if !tolerance.is_finite() || tolerance <= K::Scalar::zero() { + tolerance = Self::duplicate_relative_tolerance(); + } + + tolerance + } + + /// Rebuilds the duplicate index when a scale-aware tolerance grows beyond + /// the current grid cell size, preserving complete candidate coverage. + fn ensure_duplicate_index_cell_size( + &self, + index: Option<&mut HashGridIndex>, + tolerance: K::Scalar, + ) { + let Some(index) = index else { + return; + }; + if !HashGridIndex::::supports_dimension() + || !tolerance.is_finite() + || tolerance <= K::Scalar::zero() + { + return; + } + if index.cell_size() >= tolerance { + return; + } + + let mut rebuilt = HashGridIndex::new(tolerance); + for (vkey, vertex) in self.tds.vertices() { + rebuilt.insert_vertex(vkey, vertex.point().coords()); + } + *index = rebuilt; + } + + /// Compares a squared distance against the duplicate tolerance without + /// overflowing the tolerance square on extreme coordinate scales. + fn duplicate_distance_within_tolerance(dist_sq: K::Scalar, tolerance: K::Scalar) -> bool { + let tolerance_sq = tolerance * tolerance; + if tolerance_sq.is_finite() { + dist_sq <= tolerance_sq + } else { + dist_sq.sqrt() <= tolerance + } + } + + /// Check for near-duplicate coordinates using the hash grid when available, with a + /// linear-scan fallback (O(n·D) per insertion) if the index is unavailable/unusable. + fn duplicate_coordinates_error( + &self, + coords: &[K::Scalar; D], + tolerance: K::Scalar, + index: Option<&HashGridIndex>, + ) -> Option { + let mut duplicate_found = false; + let make_duplicate_error = || { + let mut coordinates = String::from("["); + for (idx, coord) in coords.iter().enumerate() { + if idx != 0 { + coordinates.push_str(", "); + } + let _ = write!(&mut coordinates, "{coord:?}"); + } + coordinates.push(']'); + InsertionError::DuplicateCoordinates { coordinates } + }; + + if let Some(index) = index + && index.cell_size() >= tolerance + { + let mut candidate_count = 0usize; + let used_index = index.for_each_candidate_vertex_key(coords, |vkey| { + candidate_count = candidate_count.saturating_add(1); + let Some(vertex) = self.tds.vertex(vkey) else { + return true; + }; + + let vcoords = vertex.point().coords(); + let mut dist_sq = K::Scalar::zero(); + for i in 0..D { + let diff = vcoords[i] - coords[i]; + dist_sq += diff * diff; + } + + if Self::duplicate_distance_within_tolerance(dist_sq, tolerance) { + duplicate_found = true; + return false; + } + + true + }); + record_duplicate_detection_metrics(used_index, candidate_count, !used_index); + + if duplicate_found { + return Some(make_duplicate_error()); + } + + if used_index { + return None; + } + } else { + record_duplicate_detection_metrics(false, 0, true); + } + + for (_, existing_vertex) in self.tds.vertices() { + let existing_coords = existing_vertex.point().coords(); + let mut dist_sq = K::Scalar::zero(); + for i in 0..D { + let diff = coords[i] - existing_coords[i]; + dist_sq += diff * diff; + } + + if Self::duplicate_distance_within_tolerance(dist_sq, tolerance) { + duplicate_found = true; + break; + } + } + + if duplicate_found { + Some(make_duplicate_error()) + } else { + None + } + } + + /// Estimate a local length scale for perturbation based on nearby vertices. + /// + /// Uses the hint simplex when available; otherwise falls back to the closest + /// existing vertex. This keeps perturbations translation-invariant and + /// proportional to local feature size. + fn estimate_local_perturbation_scale( + &self, + coords: &[K::Scalar; D], + hint: Option, + ) -> K::Scalar { + let mut min_dist_sq: Option = None; + + let consider_vertex = |vertex: &Vertex, + min_dist_sq: &mut Option| { + let vcoords = vertex.point().coords(); + let mut dist_sq = K::Scalar::zero(); + for i in 0..D { + let diff = vcoords[i] - coords[i]; + dist_sq += diff * diff; + } + match min_dist_sq { + Some(current) => { + if dist_sq < *current { + *current = dist_sq; + } + } + None => { + *min_dist_sq = Some(dist_sq); + } + } + }; + + if let Some(simplex_key) = hint + && let Some(simplex) = self.tds.simplex(simplex_key) + { + for &vkey in simplex.vertices() { + if let Some(vertex) = self.tds.vertex(vkey) { + consider_vertex(vertex, &mut min_dist_sq); + } + } + } + + if min_dist_sq.is_none() { + for (_, vertex) in self.tds.vertices() { + consider_vertex(vertex, &mut min_dist_sq); + } + } + + let mut scale = min_dist_sq.map_or_else(K::Scalar::one, num_traits::Float::sqrt); + + let min_scale = K::Scalar::default_tolerance(); + if scale < min_scale { + scale = min_scale; + } + + scale + } + + /// Attempt an insertion, and if Level 3 validation fails, roll back and try a + /// conservative star-split fallback of the containing simplex. + #[expect( + clippy::too_many_arguments, + reason = "Topology safety net needs transactional rollback context plus telemetry mode" + )] + fn try_insert_with_topology_safety_net( + &mut self, + vertex: Vertex, + conflict_simplices: Option<&SimplexKeyBuffer>, + hint: Option, + attempt: usize, + tds_snapshot: &Tds, + telemetry: &mut InsertionTelemetry, + telemetry_mode: InsertionTelemetryMode, + ) -> Result { + let mut insert_ok = + self.try_insert_impl(vertex, conflict_simplices, hint, telemetry, telemetry_mode)?; + + if attempt > 0 { + insert_ok.suspicion.perturbation_used = true; + } + if insert_ok.suspicion.is_suspicious() { + insert_ok.delaunay_repair_required = true; + } + + // Skip Level 3 validation during bootstrap (vertices but no simplices yet). + if self.tds.number_of_simplices() == 0 { + return Ok(insert_ok); + } + + let validation_result = self.validate_after_insertion_and_record_telemetry( + insert_ok.suspicion, + &insert_ok.repair_seed_simplices, + telemetry, + telemetry_mode, + ); + if let Err(validation_err) = validation_result { + // Roll back to snapshot and attempt a star-split fallback for interior points. + self.tds = tds_snapshot.clone_for_rollback(); + return self.try_star_split_fallback_after_topology_failure( + vertex, + hint, + attempt, + validation_err, + telemetry, + telemetry_mode, + ); + } + + Ok(insert_ok) + } + + /// After a Level 3 topology validation failure, try to recover by performing a star-split + /// of the containing simplex (if the point can be re-located inside a simplex). + /// + /// Notes: + /// - This fallback is only applicable when the point re-locates to [`LocateResult::InsideSimplex`]. + /// - We re-run Level 3 validation after the fallback to avoid "recovering" into an invalid state. + fn try_star_split_fallback_after_topology_failure( + &mut self, + vertex: Vertex, + hint: Option, + attempt: usize, + validation_err: InvariantError, + telemetry: &mut InsertionTelemetry, + telemetry_mode: InsertionTelemetryMode, + ) -> Result { + let point = *vertex.point(); + let location = match locate_with_stats(&self.tds, &self.kernel, &point, hint) { + Ok((location, stats)) => { + Self::record_locate_telemetry(telemetry, location, &stats); + Ok(location) + } + Err(error) => Err(error), + }; + + let Ok(LocateResult::InsideSimplex(start_simplex)) = location else { + return Err(Self::invariant_error_to_insertion_error(validation_err)); + }; + + let mut star_conflict = SimplexKeyBuffer::new(); + star_conflict.push(start_simplex); + + match self.try_insert_impl( + vertex, + Some(&star_conflict), + Some(start_simplex), + telemetry, + telemetry_mode, + ) { + Ok(mut fallback_ok) => { + fallback_ok.suspicion.fallback_star_split = true; + if attempt > 0 { + fallback_ok.suspicion.perturbation_used = true; + } + fallback_ok.delaunay_repair_required = true; + + let validation_result = self.validate_after_insertion_and_record_telemetry( + fallback_ok.suspicion, + &fallback_ok.repair_seed_simplices, + telemetry, + telemetry_mode, + ); + if let Err(fallback_validation_err) = validation_result { + return Err(Self::invariant_error_to_insertion_error( + fallback_validation_err, + )); + } + + // Telemetry: the fallback succeeded, meaning we recovered from a topology + // validation failure without surfacing an insertion error to the caller. + TOPOLOGY_SAFETY_NET_STAR_SPLIT_FALLBACK_SUCCESSES.fetch_add(1, Ordering::Relaxed); + + #[cfg(debug_assertions)] + tracing::debug!( + "Topology safety-net: star-split fallback succeeded (start_simplex={start_simplex:?})" + ); + + Ok(fallback_ok) + } + Err(fallback_err) => Err(fallback_err), + } + } + + /// Ensure an interior insertion never proceeds with an empty conflict region. + /// + /// An empty conflict region would produce an empty cavity boundary, create no new simplices, and + /// leave the inserted vertex isolated (not incident to any simplex), which breaks Level 3 topology + /// validation via Euler characteristic. + fn ensure_non_empty_conflict_simplices( + conflict_simplices: Cow<'_, SimplexKeyBuffer>, + fallback_simplex: SimplexKey, + ) -> Cow<'_, SimplexKeyBuffer> { + if !conflict_simplices.is_empty() { + return conflict_simplices; + } + + if let Cow::Owned(mut owned) = conflict_simplices { + owned.push(fallback_simplex); + Cow::Owned(owned) + } else { + let mut owned = SimplexKeyBuffer::new(); + owned.push(fallback_simplex); + Cow::Owned(owned) + } + } + + /// Build the boundary facets for a "star-split" of the containing simplex. + fn star_split_boundary_facets(start_simplex: SimplexKey) -> CavityBoundaryBuffer { + (0..=D) + .map(|i| { + FacetHandle::new( + start_simplex, + u8::try_from(i).expect("facet index must fit in u8"), + ) + }) + .collect() + } + + /// Reshape a conflict region until it yields a valid cavity boundary. + /// + /// Iteratively resolves cavity-boundary errors rather than immediately + /// falling back to a star-split. Star-splits create non-Delaunay + /// configurations that global flip repair must fix; in high dimensions this + /// is extremely slow. The reduction rules are: + /// + /// - `RidgeFan`: shrink by removing extra fan simplices. + /// - `DisconnectedBoundary`: expand through non-conflict neighbors, or + /// shrink if expansion cannot make progress. + /// - `OpenBoundary`: shrink by removing the simplex with the dangling facet. + #[expect( + clippy::too_many_lines, + reason = "Keep the cavity-reduction state machine together so reshape rules share one iteration budget" + )] + fn reduce_conflict_region_to_cavity_boundary( + &self, + conflict_simplices: &mut SimplexKeyBuffer, + repair_seed_simplices: &mut SimplexKeyBuffer, + delaunay_repair_required: &mut bool, + ) -> Result { + const MAX_CAVITY_ITERATIONS: usize = 32; + + let mut extraction_result = extract_cavity_boundary(&self.tds, conflict_simplices); + let mut iterations: usize = 0; + let trace_enabled = cavity_reduction_trace_enabled(); + let mut trace_cavity_reduction = false; + let mut saw_ridge_fan_shrink = false; + + match &extraction_result { + Ok(boundary) => { + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || format!("initial_ok boundary_facets={}", boundary.len()), + ); + } + Err(err) => { + trace_cavity_reduction = + trace_enabled && !CAVITY_REDUCTION_TRACE_EMITTED.swap(true, Ordering::Relaxed); + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || format!("initial_err {}", cavity_conflict_error_summary(err)), + ); + } + } + + loop { + if iterations >= MAX_CAVITY_ITERATIONS { + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || "budget_exhausted".to_string(), + ); + break; + } + iterations += 1; + + match &extraction_result { + Err(ConflictError::RidgeFan { + extra_simplices, .. + }) if !extra_simplices.is_empty() && conflict_simplices.len() > D + 1 => { + #[cfg(debug_assertions)] + tracing::debug!( + remove_count = extra_simplices.len(), + conflict_simplices_before = conflict_simplices.len(), + "D={D}: cavity reduction (RidgeFan shrink)" + ); + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || format!("ridge_fan_shrink remove_simplices={extra_simplices:?}"), + ); + saw_ridge_fan_shrink = true; + *delaunay_repair_required = true; + retain_simplices_and_record_removed( + conflict_simplices, + repair_seed_simplices, + |simplex_key| !extra_simplices.contains(&simplex_key), + ); + } + Err(ConflictError::DisconnectedBoundary { + disconnected_simplices, + .. + }) if !disconnected_simplices.is_empty() => { + let mut simplices_to_add = SimplexKeyBuffer::new(); + if !saw_ridge_fan_shrink { + for &dc in disconnected_simplices { + if let Some(simplex) = self.tds.simplex(dc) + && let Some(neighbors) = simplex.neighbor_keys() + { + for neighbor_opt in neighbors { + if let Some(nk) = neighbor_opt + && !conflict_simplices.contains(&nk) + && !simplices_to_add.contains(&nk) + { + simplices_to_add.push(nk); + } + } + } + } + } + + if !simplices_to_add.is_empty() { + *delaunay_repair_required = true; + #[cfg(debug_assertions)] + tracing::debug!( + add_count = simplices_to_add.len(), + conflict_simplices_before = conflict_simplices.len(), + "D={D}: cavity expansion (DisconnectedBoundary hole-fill)" + ); + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || { + let added: Vec = + simplices_to_add.iter().copied().collect(); + format!("disconnected_boundary_expand add_simplices={added:?}") + }, + ); + conflict_simplices.extend(simplices_to_add); + } else if conflict_simplices.len() > D + 1 { + *delaunay_repair_required = true; + #[cfg(debug_assertions)] + tracing::debug!( + remove_count = disconnected_simplices.len(), + conflict_simplices_before = conflict_simplices.len(), + "D={D}: cavity reduction (DisconnectedBoundary shrink fallback)" + ); + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || { + format!( + "disconnected_boundary_shrink remove_simplices={disconnected_simplices:?}" + ) + }, + ); + retain_simplices_and_record_removed( + conflict_simplices, + repair_seed_simplices, + |simplex_key| !disconnected_simplices.contains(&simplex_key), + ); + } else { + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || "disconnected_boundary_no_progress".to_string(), + ); + break; + } + } + Err(ConflictError::OpenBoundary { open_simplex, .. }) + if conflict_simplices.len() > D + 1 => + { + *delaunay_repair_required = true; + #[cfg(debug_assertions)] + tracing::debug!( + ?open_simplex, + conflict_simplices_before = conflict_simplices.len(), + "D={D}: cavity reduction (OpenBoundary shrink)" + ); + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || format!("open_boundary_shrink open_simplex={open_simplex:?}"), + ); + let open = *open_simplex; + retain_simplices_and_record_removed( + conflict_simplices, + repair_seed_simplices, + |simplex_key| simplex_key != open, + ); + } + _ => { + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || "no_reduction_rule_matched".to_string(), + ); + break; + } + } + + extraction_result = extract_cavity_boundary(&self.tds, conflict_simplices); + match &extraction_result { + Ok(boundary) => { + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || format!("reextract_ok boundary_facets={}", boundary.len()), + ); + } + Err(err) => { + log_cavity_reduction_event( + trace_cavity_reduction, + iterations, + conflict_simplices, + || format!("reextract_err {}", cavity_conflict_error_summary(err)), + ); + } + } + } + + extraction_result + } + + /// Perform cavity insertion given an explicit conflict region. + #[expect( + clippy::too_many_lines, + reason = "Keep cavity insertion and repair logic together for clarity" + )] + fn insert_with_conflict_region( + &mut self, + v_key: VertexKey, + point: &Point, + mut conflict_simplices: SimplexKeyBuffer, + fallback_simplex: Option, + suspicion: &mut SuspicionFlags, + ) -> Result { + #[cfg(not(debug_assertions))] + let _ = point; + + if conflict_simplices.is_empty() { + let Some(start_simplex) = fallback_simplex else { + return Err(CavityFillingError::EmptyConflictRegion { + fallback_simplex: None, + } + .into()); + }; + suspicion.empty_conflict_region = true; + suspicion.fallback_star_split = true; + conflict_simplices.push(start_simplex); + // The fallback star-split is topologically safe but not a full + // Bowyer-Watson conflict-region replacement, so local Delaunay + // repair must revisit it. + } + + // Preserve every simplex that participates in cavity shaping and is later + // removed from the final cavity so callers can seed local Delaunay + // repair from the surviving fringe. + let mut repair_seed_simplices = SimplexKeyBuffer::new(); + let mut delaunay_repair_required = suspicion.fallback_star_split; + + let mut boundary_facets = match self.reduce_conflict_region_to_cavity_boundary( + &mut conflict_simplices, + &mut repair_seed_simplices, + &mut delaunay_repair_required, + ) { + Ok(boundary) => boundary, + Err(err) => { + // For D=3 and D>=4: do NOT fall back to star-split once cavity reduction + // is exhausted. Star-splits create heavily non-Delaunay configurations + // whose isolated violations may not be connected to the star-split star + // through any violation chain. Return a retryable error instead so + // insert_transactional can retry with a perturbed vertex and, after all + // retries, skip the vertex. + // + // For D=2: star-split is used as a last resort. The 2D flip repair + // guarantees convergence from star-split configurations and the extra + // simplices are quickly handled by the k=2 repair loop. + let should_fallback = D < 3 + && matches!( + err, + ConflictError::NonManifoldFacet { .. } + | ConflictError::RidgeFan { .. } + | ConflictError::DisconnectedBoundary { .. } + | ConflictError::OpenBoundary { .. } + ); + + if should_fallback { + let Some(start_simplex) = fallback_simplex else { + return Err(err.into()); + }; + + suspicion.fallback_star_split = true; + delaunay_repair_required = true; + + #[cfg(debug_assertions)] + tracing::warn!( + "Conflict region degeneracy ({err}); falling back to star-split of simplex {start_simplex:?}" + ); + + let mut replacement = SimplexKeyBuffer::new(); + replacement.push(start_simplex); + replace_simplices_and_record_removed( + &mut conflict_simplices, + &mut repair_seed_simplices, + replacement, + ); + + Self::star_split_boundary_facets(start_simplex) + } else { + #[cfg(debug_assertions)] + tracing::debug!( + "D={D}: cavity boundary unresolvable ({err}); returning retryable error" + ); + return Err(err.into()); + } + } + }; + + // Fallback: never allow an insertion to create a dangling vertex. + if boundary_facets.is_empty() { + let Some(start_simplex) = fallback_simplex else { + return Err(CavityFillingError::EmptyBoundary { + fallback_simplex: None, + } + .into()); + }; + + suspicion.empty_conflict_region = true; + suspicion.fallback_star_split = true; + delaunay_repair_required = true; + + #[cfg(debug_assertions)] + tracing::warn!( + "Empty cavity boundary; falling back to splitting containing simplex {start_simplex:?}" + ); + + let mut replacement = SimplexKeyBuffer::new(); + replacement.push(start_simplex); + replace_simplices_and_record_removed( + &mut conflict_simplices, + &mut repair_seed_simplices, + replacement, + ); + boundary_facets = Self::star_split_boundary_facets(start_simplex); + } + + // Fill cavity BEFORE removing old simplices. + let new_simplices = + fill_cavity_replacing_simplices(&mut self.tds, v_key, &boundary_facets)?; + self.canonicalize_positive_orientation_for_simplices(&new_simplices)?; + + // Post-insertion orientation audit: verify that canonicalization + // actually produced all-positive orientations among the new simplices. + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_ORIENTATION").is_some() { + let mut pos = 0_usize; + let mut neg = 0_usize; + let mut deg = 0_usize; + let mut fail = 0_usize; + for &ck in &new_simplices { + if let Some(c) = self.tds.simplex(ck) { + match self.evaluate_simplex_orientation_for_context( + ck, + c, + "post-insertion orientation audit", + "orientation predicate failed during post-insertion audit", + ) { + Ok(o) if o > 0 => pos += 1, + Ok(o) if o < 0 => neg += 1, + Ok(_) => deg += 1, + Err(ref e) => { + fail += 1; + tracing::warn!( + simplex_key = ?ck, + error = %e, + "post-insertion orientation audit: evaluation failed" + ); + } + } + } + } + if neg > 0 || fail > 0 { + tracing::warn!( + new_simplices = new_simplices.len(), + positive = pos, + negative = neg, + degenerate = deg, + eval_errors = fail, + "post-insertion orientation audit: NEGATIVE simplices or evaluation errors after canonicalization" + ); + } else { + tracing::debug!( + new_simplices = new_simplices.len(), + positive = pos, + degenerate = deg, + "post-insertion orientation audit: all simplices positive" + ); + } + } + + // Wire neighbors (while both old and new simplices exist) + let external_facets = + external_facets_for_boundary(&self.tds, &conflict_simplices, &boundary_facets)?; + wire_cavity_neighbors( + &mut self.tds, + &new_simplices, + external_facets.iter().copied(), + Some(&conflict_simplices), + )?; + + // Drop any repair-seed entries that were removed earlier but later got + // reintroduced into the final conflict region. Those keys will be + // deleted by `remove_simplices_by_keys` below, so they cannot seed repair. + repair_seed_simplices.retain(|ck| !conflict_simplices.contains(ck)); + let mut seen_repair_seed_simplices = SimplexKeyBuffer::new(); + repair_seed_simplices.retain(|ck| { + if seen_repair_seed_simplices.contains(ck) { + false + } else { + seen_repair_seed_simplices.push(*ck); + true + } + }); + + // Remove conflict simplices (now that new simplices are wired up) + let _removed_count = self.tds.remove_simplices_by_keys(&conflict_simplices); + + // Iteratively repair non-manifold topology until facet sharing is valid + let mut total_removed = 0; + let mut facet_sharing_known_valid = true; + let mut neighbor_repair_frontier = SimplexKeyBuffer::new(); + #[cfg_attr( + not(debug_assertions), + expect( + unused_variables, + reason = "`iteration` is only used for debug logging", + ) + )] + for iteration in 0..MAX_REPAIR_ITERATIONS { + // Check for non-manifold issues in newly created simplices (local scan) + let simplices_to_check: SimplexKeyBuffer = new_simplices + .iter() + .copied() + .filter(|ck| self.tds.contains_simplex(*ck)) + .collect(); + + if let Some(issues) = self.detect_local_facet_issues(&simplices_to_check)? { + // Only mark this as "suspicious" if we *actually* detected local facet issues + // and entered the repair path. + suspicion.repair_loop_entered = true; + delaunay_repair_required = true; + + #[cfg(debug_assertions)] + tracing::debug!( + "Repair iteration {}: {} over-shared facets detected, removing simplices...", + iteration + 1, + issues.len() + ); + + let repair = self.repair_local_facet_issues_with_frontier(&issues)?; + let removed = repair.removed_count; + + // Early exit if repair made no progress + if removed == 0 { + #[cfg(debug_assertions)] + tracing::warn!( + "No simplices removed in iteration {} - repair cannot make progress", + iteration + 1 + ); + return Err(InsertionError::TopologyValidation( + TdsError::InconsistentDataStructure { + message: format!( + "Repair stalled: {} over-shared facets remain but no simplices could be removed", + issues.len() + ), + }, + )); + } + + total_removed += removed; + neighbor_repair_frontier.extend(repair.frontier_simplices); + + if removed > 0 { + suspicion.simplices_removed = true; + delaunay_repair_required = true; + } + + #[cfg(debug_assertions)] + tracing::debug!( + removed_simplices = ?repair.removed_simplices, + "Removed {removed} simplices (total: {total_removed})" + ); + + // Early exit if repair succeeded + facet_sharing_known_valid = self.tds.validate_facet_sharing().is_ok(); + if facet_sharing_known_valid { + break; + } + } else { + // No more non-manifold issues - safe to rebuild neighbors + break; + } + } + + // Rebuild neighbor pointers now that topology is manifold. + #[cfg(debug_assertions)] + tracing::debug!("After repair loop: total_removed={total_removed}"); + + if !facet_sharing_known_valid { + return Err(CavityFillingError::InvalidFacetSharingAfterRepair { + stage: CavityRepairStage::PrimaryInsertion, + } + .into()); + } + + // Global neighbor rebuild is expensive. In the common case (no simplices removed during the + // local facet-repair loop), `wire_cavity_neighbors` has already glued the cavity locally. + // + // If we *did* remove simplices during the repair loop, repair only the new-simplex/frontier + // neighborhood unless the force-rebuild diagnostic environment variable is set. + if total_removed > 0 { + let repaired = self.repair_neighbors_after_local_simplex_removal( + &new_simplices, + &neighbor_repair_frontier, + )?; + suspicion.neighbor_pointers_rebuilt = repaired > 0; + delaunay_repair_required = true; + } + + // New cavity simplices were canonicalized on creation; validate the local + // orientation frontier without scanning the whole triangulation. + let mut orientation_simplices = SimplexKeyBuffer::new(); + append_live_unique_simplex_seeds(&self.tds, &new_simplices, &mut orientation_simplices); + append_live_unique_simplex_seeds( + &self.tds, + &neighbor_repair_frontier, + &mut orientation_simplices, + ); + self.validate_local_orientation_for_simplices(&orientation_simplices)?; + + // Assign an incident simplex for the inserted vertex without a global rebuild. + let hint = new_simplices.iter().copied().find(|&ck| { + self.tds + .simplex(ck) + .is_some_and(|simplex| simplex.contains_vertex(v_key)) + }); + if let Some(incident_simplex) = hint + && let Some(vertex) = self.tds.vertex_mut(v_key) + { + vertex.set_incident_simplex(Some(incident_simplex)); + } + + // Optional debug: validate neighbor pointers by forcing a full facet walk (no hint). + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_VALIDATE_LOCATE").is_some() { + let _ = locate(&self.tds, &self.kernel, point, None)?; + } + + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_RIDGE_LINK").is_some() { + match validate_ridge_links(&self.tds) { + Ok(()) => { + tracing::debug!( + "insert_with_conflict_region: ridge-link validation passed after insertion" + ); + } + Err(err) => { + tracing::warn!( + error = ?err, + "insert_with_conflict_region: ridge-link validation failed after insertion" + ); + } + } + } + + // Repair stale incident-simplex pointers (e.g. pointing to deleted conflict-region + // simplices) and error only for truly isolated vertices (in zero simplices). + self.repair_stale_incident_simplices()?; + + // Connectedness guard (STRUCTURAL SAFETY, NOT Level 3 validation) + self.validate_connectedness(&new_simplices)?; + + // Seed follow-up Delaunay repair from the local insertion product. Higher layers + // use these simplices to avoid rediscovering the inserted vertex star with a global scan. + append_live_unique_simplex_seeds(&self.tds, &new_simplices, &mut repair_seed_simplices); + append_live_unique_simplex_seeds( + &self.tds, + &neighbor_repair_frontier, + &mut repair_seed_simplices, + ); + + // Return hint for next insertion + Ok(CavityInsertionOutcome { + hint, + simplices_removed: total_removed, + repair_seed_simplices, + delaunay_repair_required: delaunay_repair_required || suspicion.is_suspicious(), + }) + } + + /// Records one point-location result into insertion telemetry. + #[inline] + fn record_locate_telemetry( + telemetry: &mut InsertionTelemetry, + location: LocateResult, + stats: &LocateStats, + ) { + telemetry.locate_calls = telemetry.locate_calls.saturating_add(1); + telemetry.locate_walk_steps_total = telemetry + .locate_walk_steps_total + .saturating_add(stats.walk_steps); + telemetry.locate_walk_steps_max = telemetry.locate_walk_steps_max.max(stats.walk_steps); + + if stats.used_hint { + telemetry.locate_hint_uses = telemetry.locate_hint_uses.saturating_add(1); + } + + if stats.fell_back_to_scan() { + telemetry.locate_scan_fallbacks = telemetry.locate_scan_fallbacks.saturating_add(1); + } + + match location { + LocateResult::InsideSimplex(_) => { + telemetry.located_inside = telemetry.located_inside.saturating_add(1); + } + LocateResult::Outside => { + telemetry.located_outside = telemetry.located_outside.saturating_add(1); + } + LocateResult::OnFacet(_, _) | LocateResult::OnEdge(_) | LocateResult::OnVertex(_) => { + telemetry.located_on_boundary = telemetry.located_on_boundary.saturating_add(1); + } + } + } + + /// Records conflict-region size counters without touching timing fields. + #[inline] + fn record_conflict_region_telemetry(telemetry: &mut InsertionTelemetry, simplices: usize) { + telemetry.conflict_region_calls = telemetry.conflict_region_calls.saturating_add(1); + telemetry.conflict_region_simplices_total = telemetry + .conflict_region_simplices_total + .saturating_add(simplices); + telemetry.conflict_region_simplices_max = + telemetry.conflict_region_simplices_max.max(simplices); + } + + /// Records measured conflict-region time when timing telemetry is enabled. + #[inline] + fn record_conflict_region_timing(telemetry: &mut InsertionTelemetry, elapsed_nanos: u64) { + telemetry.conflict_region_nanos = telemetry + .conflict_region_nanos + .saturating_add(elapsed_nanos); + telemetry.conflict_region_nanos_max = + telemetry.conflict_region_nanos_max.max(elapsed_nanos); + } + + /// Records one cavity insertion attempt and its optional elapsed time. + #[inline] + fn record_cavity_insertion_telemetry( + telemetry: &mut InsertionTelemetry, + elapsed_nanos: Option, + ) { + telemetry.cavity_insertion_calls = telemetry.cavity_insertion_calls.saturating_add(1); + if let Some(elapsed_nanos) = elapsed_nanos { + telemetry.cavity_insertion_nanos = telemetry + .cavity_insertion_nanos + .saturating_add(elapsed_nanos); + telemetry.cavity_insertion_nanos_max = + telemetry.cavity_insertion_nanos_max.max(elapsed_nanos); + } + } + + /// Records one hull-extension attempt and its optional elapsed time. + #[inline] + fn record_hull_extension_telemetry( + telemetry: &mut InsertionTelemetry, + elapsed_nanos: Option, + ) { + telemetry.hull_extension_calls = telemetry.hull_extension_calls.saturating_add(1); + if let Some(elapsed_nanos) = elapsed_nanos { + telemetry.hull_extension_nanos = + telemetry.hull_extension_nanos.saturating_add(elapsed_nanos); + telemetry.hull_extension_nanos_max = + telemetry.hull_extension_nanos_max.max(elapsed_nanos); + } + } + + /// Convert a duration to nanoseconds while saturating at `u64::MAX`. + #[inline] + fn duration_nanos_saturating(duration: Duration) -> u64 { + u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX) + } + + /// Starts a wall-clock timer only when insertion telemetry will publish timings. + #[inline] + fn start_insertion_timing(telemetry_mode: InsertionTelemetryMode) -> Option { + telemetry_mode.records_timings().then(Instant::now) + } + + fn collect_exterior_repair_seed_simplices( + &self, + point: &Point, + terminal_simplex: SimplexKey, + locate_stats: &LocateStats, + telemetry_mode: InsertionTelemetryMode, + telemetry: &mut InsertionTelemetry, + ) -> Result { + if locate_stats.fell_back_to_scan() || !self.tds.contains_simplex(terminal_simplex) { + return Ok(SimplexKeyBuffer::new()); + } + + let conflict_started = Self::start_insertion_timing(telemetry_mode); + let local_seed_simplices = collect_local_exterior_conflict_seed_simplices( + &self.tds, + &self.kernel, + point, + terminal_simplex, + )?; + Self::record_conflict_region_telemetry( + telemetry, + local_seed_simplices.conflict_simplices_found, + ); + if let Some(conflict_started) = conflict_started { + Self::record_conflict_region_timing( + telemetry, + Self::duration_nanos_saturating(conflict_started.elapsed()), + ); + } + Ok(local_seed_simplices.seed_simplices) + } + + /// Internal implementation of insert without retry logic. + /// Returns the result and the number of simplices removed during repair. + /// + /// Note: `conflict_simplices` parameter is optional. If `None`, it will be computed automatically + /// for interior points using `locate()` + `find_conflict_region()`. + #[expect( + clippy::too_many_lines, + reason = "Complex insertion logic; splitting further would harm readability" + )] + fn try_insert_impl( + &mut self, + vertex: Vertex, + conflict_simplices: Option<&SimplexKeyBuffer>, + hint: Option, + telemetry: &mut InsertionTelemetry, + telemetry_mode: InsertionTelemetryMode, + ) -> Result { + let mut suspicion = SuspicionFlags::default(); + + // CRITICAL: Capture UUID and point BEFORE inserting into TDS + // Rationale: + // - inserted_uuid: Needed to remap v_key after TDS rebuild (lines 736-744) + // when building initial simplex. The rebuild replaces self.tds entirely, + // invalidating all previous VertexKeys. + // - point: Needed for locate(), find_conflict_region(), and extend_hull() calls + // (lines 752, 760, 879, 895). After TDS rebuild, we cannot access the vertex + // via the old v_key, so we must have the point value captured. + let inserted_uuid = vertex.uuid(); + let point = *vertex.point(); + + vertex.is_valid().map_err(|source| { + InsertionError::TopologyValidation(TdsError::InvalidVertex { + vertex_id: inserted_uuid, + source, + }) + })?; + + // 1. Insert vertex into Tds + let mut v_key = self + .tds + .insert_vertex_with_mapping(vertex) + .map_err(InsertionError::from)?; + + // 2. Check if we need to bootstrap the initial simplex + let num_vertices = self.tds.number_of_vertices(); + + if num_vertices < D + 1 { + // Bootstrap phase: just accumulate vertices, no simplices yet + return Ok(TryInsertImplOk { + inserted: (v_key, None), + simplices_removed: 0, + suspicion, + repair_seed_simplices: SimplexKeyBuffer::new(), + delaunay_repair_required: false, + }); + } else if num_vertices == D + 1 { + // Build initial simplex from all D+1 vertices + let all_vertices: Vec<_> = self.tds.vertices().map(|(_, v)| *v).collect(); + let new_tds = Self::build_initial_simplex(&all_vertices).map_err(|source| { + CavityFillingError::InitialSimplexConstruction { + reason: source.into(), + } + })?; + + // Replace empty TDS with simplex TDS (preserve kernel) + self.tds = new_tds; + + // Re-map vertex key to the rebuilt TDS + v_key = self.tds.vertex_key_from_uuid(&inserted_uuid).ok_or( + CavityFillingError::RebuiltVertexMissing { + uuid: inserted_uuid, + }, + )?; + + // Return first simplex key for hint caching + let first_simplex = self.tds.simplex_keys().next(); + return Ok(TryInsertImplOk { + inserted: (v_key, first_simplex), + simplices_removed: 0, + suspicion, + repair_seed_simplices: SimplexKeyBuffer::new(), + delaunay_repair_required: false, + }); + } + + // 3. Locate containing simplex (for vertex D+2 and beyond). + // + // `locate()` delegates to `locate_with_stats()`, so collecting the stats here keeps + // the same point-location algorithm while making release-mode batch diagnostics useful. + let locate_trace = locate_with_trace(&self.tds, &self.kernel, &point, hint)?; + let location = locate_trace.result; + let locate_stats = locate_trace.stats; + Self::record_locate_telemetry(telemetry, location, &locate_stats); + + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_HULL").is_some() + || env::var_os("DELAUNAY_DEBUG_LOCATE").is_some() + { + tracing::debug!( + point = ?point, + location = ?location, + start_simplex = ?locate_stats.start_simplex, + used_hint = locate_stats.used_hint, + walk_steps = locate_stats.walk_steps, + fallback = ?locate_stats.fallback, + "try_insert_impl: locate stats" + ); + } + + // 4. Determine the supported insertion site and any conflict simplices it needs. + let insertion_site = match (location, conflict_simplices) { + (LocateResult::InsideSimplex(start_simplex), None) => { + // Interior point: compute conflict region automatically. + // + // IMPORTANT: + // `find_conflict_region()` (Bowyer–Watson style) can legitimately return an empty + // set when the point lies inside the triangulation but is not strictly inside any + // existing simplex circumsphere (e.g., obtuse tetrahedra whose circumsphere does not + // contain all interior points). + // + // An empty conflict region would produce an empty cavity boundary, create no new + // simplices, and leave the inserted vertex isolated (not incident to any simplex), which + // breaks Level 3 topology validation via Euler characteristic. + // + // Fallback: treat the containing simplex as the conflict region, effectively performing + // a star-split of that simplex to keep the simplicial complex connected. + let conflict_started = Self::start_insertion_timing(telemetry_mode); + let computed = + find_conflict_region(&self.tds, &self.kernel, &point, start_simplex)?; + Self::record_conflict_region_telemetry(telemetry, computed.len()); + if let Some(conflict_started) = conflict_started { + Self::record_conflict_region_timing( + telemetry, + Self::duration_nanos_saturating(conflict_started.elapsed()), + ); + } + + #[cfg(feature = "diagnostics")] + if env::var_os("DELAUNAY_DEBUG_CONFLICT_VERIFY").is_some() { + let missed = verify_conflict_region_completeness( + &self.tds, + &self.kernel, + &point, + &computed, + ); + if missed > 0 { + tracing::warn!( + missed, + bfs_conflict = computed.len(), + start_simplex = ?start_simplex, + point = ?point, + num_vertices = self.tds.number_of_vertices(), + num_simplices = self.tds.number_of_simplices(), + "try_insert_impl: INCOMPLETE conflict region at insertion" + ); + } + } + + if computed.is_empty() { + suspicion.empty_conflict_region = true; + suspicion.fallback_star_split = true; + } + InsertionSite::Interior { + start_simplex, + conflict_simplices: Self::ensure_non_empty_conflict_simplices( + Cow::Owned(computed), + start_simplex, + ), + } + } + (LocateResult::InsideSimplex(start_simplex), Some(simplices)) => { + // If the caller provided an empty conflict region (can happen if the Delaunay layer + // computes conflicts using a strict in-sphere test), we must still replace at least + // one simplex; otherwise we'd create no cavity, no new simplices, and leave a dangling + // vertex (χ increases by 1, typically showing up as χ=2 for Ball(3)). + if simplices.is_empty() { + suspicion.empty_conflict_region = true; + suspicion.fallback_star_split = true; + } + InsertionSite::Interior { + start_simplex, + conflict_simplices: Self::ensure_non_empty_conflict_simplices( + Cow::Borrowed(simplices), + start_simplex, + ), + } + } + (LocateResult::Outside, None) => { + // Exterior insertion is the hull-extension case. Avoid the old + // full-TDS conflict scan here; it was O(number_of_simplices) per + // exterior point, often only to rediscover that the hull path + // was required anyway. Cadenced and final Delaunay repair own + // any local empty-circumsphere cleanup after the hull mutation. + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_HULL").is_some() { + tracing::debug!( + "Outside insertion: skipping global conflict-region scan; using hull extension" + ); + } + let repair_seed_simplices = self.collect_exterior_repair_seed_simplices( + &point, + locate_trace.terminal_simplex, + &locate_stats, + telemetry_mode, + telemetry, + )?; + InsertionSite::Exterior { + conflict_simplices: None, + repair_seed_simplices, + } + } + (LocateResult::Outside, Some(simplices)) => { + if simplices.is_empty() { + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_HULL").is_some() { + tracing::debug!( + "Outside insertion: caller provided empty conflict region; will use hull extension" + ); + } + let repair_seed_simplices = self.collect_exterior_repair_seed_simplices( + &point, + locate_trace.terminal_simplex, + &locate_stats, + telemetry_mode, + telemetry, + )?; + InsertionSite::Exterior { + conflict_simplices: None, + repair_seed_simplices, + } + } else { + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_HULL").is_some() { + tracing::debug!( + conflict_simplices = simplices.len(), + "Outside insertion: using caller-provided conflict region" + ); + } + InsertionSite::Exterior { + conflict_simplices: Some(Cow::Borrowed(simplices)), + repair_seed_simplices: simplices.iter().copied().collect(), + } + } + } + (location, _) => { + // Degenerate locations (OnFacet, OnEdge, OnVertex) + return Err(CavityFillingError::UnsupportedDegenerateLocation { location }.into()); + } + }; + + // 5. Handle different insertion sites. + match insertion_site { + InsertionSite::Interior { + start_simplex, + conflict_simplices, + } => { + let conflict_simplices = conflict_simplices.into_owned(); + let cavity_started = Self::start_insertion_timing(telemetry_mode); + let insertion_result = self.insert_with_conflict_region( + v_key, + &point, + conflict_simplices, + Some(start_simplex), + &mut suspicion, + ); + Self::record_cavity_insertion_telemetry( + telemetry, + cavity_started + .map(|started| Self::duration_nanos_saturating(started.elapsed())), + ); + let outcome = insertion_result?; + Ok(TryInsertImplOk { + inserted: (v_key, outcome.hint), + simplices_removed: outcome.simplices_removed, + suspicion, + repair_seed_simplices: outcome.repair_seed_simplices, + delaunay_repair_required: outcome.delaunay_repair_required, + }) + } + InsertionSite::Exterior { + conflict_simplices, + repair_seed_simplices: exterior_repair_seed_simplices, + } => { + if let Some(conflict_simplices) = conflict_simplices { + let conflict_simplices = conflict_simplices.into_owned(); + #[cfg(debug_assertions)] + let conflict_len = conflict_simplices.len(); + #[cfg(debug_assertions)] + tracing::debug!( + "Outside insertion attempting cavity insertion with conflict region size {conflict_len}" + ); + let cavity_started = Self::start_insertion_timing(telemetry_mode); + let result = self.insert_with_conflict_region( + v_key, + &point, + conflict_simplices, + None, + &mut suspicion, + ); + Self::record_cavity_insertion_telemetry( + telemetry, + cavity_started + .map(|started| Self::duration_nanos_saturating(started.elapsed())), + ); + match result { + Ok(outcome) => { + return Ok(TryInsertImplOk { + inserted: (v_key, outcome.hint), + simplices_removed: outcome.simplices_removed, + suspicion, + repair_seed_simplices: outcome.repair_seed_simplices, + delaunay_repair_required: true, + }); + } + Err(err) => { + // For exterior points, a "global" conflict region can intersect the hull, + // producing an open/disconnected cavity boundary. In these cases we fall back + // to hull extension instead of surfacing an insertion error. + // + // IMPORTANT: Only ConflictError variants are safe to fall back from here. + // These originate from `extract_cavity_boundary` which runs BEFORE any TDS + // mutation. Errors like `IsolatedVertex` originate from AFTER the cavity + // has been filled, neighbors wired, and conflict simplices removed — the TDS + // is already heavily mutated and hull extension on that state is unsound. + let should_fallback = matches!( + &err, + InsertionError::ConflictRegion( + ConflictError::NonManifoldFacet { .. } + | ConflictError::RidgeFan { .. } + | ConflictError::DisconnectedBoundary { .. } + | ConflictError::OpenBoundary { .. } + ) + ); + + if should_fallback { + #[cfg(debug_assertions)] + tracing::warn!( + "Outside insertion conflict boundary degeneracy ({err}) (conflict_simplices={conflict_len}); falling back to hull extension" + ); + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_HULL").is_some() { + tracing::debug!( + error = %err, + "Outside insertion: cavity insertion failed; using hull extension" + ); + } + } else { + #[cfg(debug_assertions)] + tracing::warn!("Outside insertion cavity insertion failed: {err}"); + return Err(err); + } + } + } + } + // Exterior vertex: extend convex hull + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_HULL").is_some() { + tracing::debug!( + point = ?point, + "Outside insertion: proceeding to hull extension" + ); + } + let hull_started = Self::start_insertion_timing(telemetry_mode); + let hull_result = extend_hull(&mut self.tds, &self.kernel, v_key, &point); + Self::record_hull_extension_telemetry( + telemetry, + hull_started.map(|started| Self::duration_nanos_saturating(started.elapsed())), + ); + let new_simplices = match hull_result { + Ok(simplices) => simplices, + Err(err) => { + let retry_inside = matches!( + &err, + InsertionError::HullExtension { + reason: HullExtensionReason::NoVisibleFacets + } + ); + if retry_inside { + let fallback_location = + locate_by_scan(&self.tds, &self.kernel, &point)?; + // This retry starts as a scan, so account for the fallback + // explicitly and let the common recorder handle the outcome. + telemetry.locate_scan_fallbacks = + telemetry.locate_scan_fallbacks.saturating_add(1); + let scan_start_simplex = self + .tds + .simplex_keys() + .next() + .ok_or(LocateError::EmptyTriangulation)?; + let scan_stats = LocateStats { + start_simplex: scan_start_simplex, + used_hint: false, + walk_steps: 0, + fallback: None, + }; + Self::record_locate_telemetry( + telemetry, + fallback_location, + &scan_stats, + ); + if let LocateResult::InsideSimplex(start_simplex) = fallback_location { + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_HULL").is_some() { + tracing::warn!( + point = ?point, + start_simplex = ?start_simplex, + "Outside insertion: no visible facets; retrying as interior with star-split" + ); + } + suspicion.fallback_star_split = true; + let mut star_conflict = SimplexKeyBuffer::new(); + star_conflict.push(start_simplex); + let cavity_started = Self::start_insertion_timing(telemetry_mode); + let insertion_result = self.insert_with_conflict_region( + v_key, + &point, + star_conflict, + Some(start_simplex), + &mut suspicion, + ); + Self::record_cavity_insertion_telemetry( + telemetry, + cavity_started.map(|started| { + Self::duration_nanos_saturating(started.elapsed()) + }), + ); + let outcome = insertion_result?; + return Ok(TryInsertImplOk { + inserted: (v_key, outcome.hint), + simplices_removed: outcome.simplices_removed, + suspicion, + repair_seed_simplices: outcome.repair_seed_simplices, + delaunay_repair_required: true, + }); + } + } + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_HULL").is_some() { + tracing::warn!( + point = ?point, + error = %err, + "Outside insertion: hull extension failed" + ); + } + return Err(err); + } + }; + self.canonicalize_positive_orientation_for_simplices(&new_simplices)?; + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_HULL").is_some() { + tracing::debug!( + new_simplices = new_simplices.len(), + "Outside insertion: hull extension succeeded" + ); + } + + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_NEIGHBORS").is_some() { + let mut total_slots = 0usize; + let mut neighbor_none = 0usize; + let mut neighbor_missing = 0usize; + let mut neighbor_mutual = 0usize; + let mut neighbor_non_mutual = 0usize; + + for &simplex_key in &new_simplices { + let Some(simplex) = self.tds.simplex(simplex_key) else { + continue; + }; + let Some(neighbors) = simplex.neighbor_keys() else { + continue; + }; + for neighbor_opt in neighbors { + total_slots = total_slots.saturating_add(1); + match neighbor_opt { + None => { + neighbor_none = neighbor_none.saturating_add(1); + } + Some(neighbor_key) => { + if !self.tds.contains_simplex(neighbor_key) { + neighbor_missing = neighbor_missing.saturating_add(1); + } else if self + .tds + .simplex(neighbor_key) + .and_then(Simplex::neighbors) + .is_some_and(|mut ns| { + ns.any(|neighbor| neighbor == Some(simplex_key)) + }) + { + neighbor_mutual = neighbor_mutual.saturating_add(1); + } else { + neighbor_non_mutual = neighbor_non_mutual.saturating_add(1); + } + } + } + } + } + + tracing::debug!( + new_simplices = new_simplices.len(), + total_slots, + neighbor_none, + neighbor_missing, + neighbor_mutual, + neighbor_non_mutual, + "Outside insertion: hull extension neighbor-pointer summary" + ); + } + + // Iteratively repair non-manifold topology until facet sharing is valid + let mut total_removed = 0; + let mut facet_sharing_known_valid = true; + let mut neighbor_repair_frontier = SimplexKeyBuffer::new(); + #[cfg_attr( + not(debug_assertions), + expect( + unused_variables, + reason = "`iteration` is only used for debug logging", + ) + )] + for iteration in 0..MAX_REPAIR_ITERATIONS { + // Check for non-manifold issues in newly created hull simplices (local scan) + // This keeps the repair O(k·D) where k is the number of new hull simplices, rather than O(N·D) + let simplices_to_check: SimplexKeyBuffer = new_simplices + .iter() + .copied() + .filter(|ck| self.tds.contains_simplex(*ck)) + .collect(); + + if let Some(issues) = self.detect_local_facet_issues(&simplices_to_check)? { + // Only mark this as "suspicious" if we *actually* detected local facet issues + // and entered the repair path. + suspicion.repair_loop_entered = true; + + #[cfg(debug_assertions)] + tracing::debug!( + "Hull extension repair iteration {}: {} over-shared facets detected, removing simplices...", + iteration + 1, + issues.len() + ); + + let repair = self.repair_local_facet_issues_with_frontier(&issues)?; + let removed = repair.removed_count; + + // Early exit if repair made no progress + if removed == 0 { + #[cfg(debug_assertions)] + tracing::warn!( + "No simplices removed in iteration {} - repair cannot make progress", + iteration + 1 + ); + return Err(InsertionError::TopologyValidation( + TdsError::InconsistentDataStructure { + message: format!( + "Hull extension repair stalled: {} over-shared facets remain but no simplices could be removed", + issues.len() + ), + }, + )); + } + + total_removed += removed; + neighbor_repair_frontier.extend(repair.frontier_simplices); + if removed > 0 { + suspicion.simplices_removed = true; + } + + #[cfg(debug_assertions)] + tracing::debug!( + removed_simplices = ?repair.removed_simplices, + "Removed {removed} simplices (total: {total_removed})" + ); + + // Early exit if repair succeeded + facet_sharing_known_valid = self.tds.validate_facet_sharing().is_ok(); + if facet_sharing_known_valid { + break; + } + } else { + // No more non-manifold issues - safe to rebuild neighbors + break; + } + } + + // Repair neighbor pointers now that topology is manifold. + if !facet_sharing_known_valid { + return Err(CavityFillingError::InvalidFacetSharingAfterRepair { + stage: CavityRepairStage::FanTriangulation, + } + .into()); + } + + if total_removed > 0 { + let repaired = self.repair_neighbors_after_local_simplex_removal( + &new_simplices, + &neighbor_repair_frontier, + )?; + suspicion.neighbor_pointers_rebuilt = repaired > 0; + } + + // New hull simplices were canonicalized on creation; validate the + // local orientation frontier without scanning the whole TDS. + let mut orientation_simplices = SimplexKeyBuffer::new(); + append_live_unique_simplex_seeds( + &self.tds, + &new_simplices, + &mut orientation_simplices, + ); + append_live_unique_simplex_seeds( + &self.tds, + &neighbor_repair_frontier, + &mut orientation_simplices, + ); + self.validate_local_orientation_for_simplices(&orientation_simplices)?; + + // Assign an incident simplex for the inserted vertex without a global rebuild. + let hint = new_simplices.iter().copied().find(|&ck| { + self.tds + .simplex(ck) + .is_some_and(|simplex| simplex.contains_vertex(v_key)) + }); + if let Some(incident_simplex) = hint + && let Some(vertex) = self.tds.vertex_mut(v_key) + { + vertex.set_incident_simplex(Some(incident_simplex)); + } + + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_RIDGE_LINK").is_some() { + match validate_ridge_links(&self.tds) { + Ok(()) => { + tracing::debug!( + "extend_hull: ridge-link validation passed after insertion" + ); + } + Err(err) => { + tracing::warn!( + error = ?err, + "extend_hull: ridge-link validation failed after insertion" + ); + } + } + } + + // Repair stale incident-simplex pointers (e.g. pointing to deleted + // conflict-region simplices) and error only for truly isolated vertices. + self.repair_stale_incident_simplices()?; + + // Connectedness guard (localized): ensure the newly created simplex set is internally + // connected and attached to the existing triangulation. + self.validate_connectedness(&new_simplices)?; + + // Return vertex key and hint for next insertion + let mut repair_seed_simplices = SimplexKeyBuffer::new(); + append_live_unique_simplex_seeds( + &self.tds, + &new_simplices, + &mut repair_seed_simplices, + ); + append_live_unique_simplex_seeds( + &self.tds, + &neighbor_repair_frontier, + &mut repair_seed_simplices, + ); + append_live_unique_simplex_seeds( + &self.tds, + &exterior_repair_seed_simplices, + &mut repair_seed_simplices, + ); + Ok(TryInsertImplOk { + inserted: (v_key, hint), + simplices_removed: total_removed, + suspicion, + repair_seed_simplices, + delaunay_repair_required: true, + }) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::algorithms::locate::InternalInconsistencySite; + use crate::core::collections::spatial_hash_grid::HashGridIndex; + use crate::core::simplex::Simplex; + use crate::core::vertex::VertexBuilder; + use crate::geometry::kernel::{AdaptiveKernel, FastKernel}; + use crate::geometry::point::Point; + use crate::geometry::traits::coordinate::{ + Coordinate, CoordinateConversionError, CoordinateScalar, + }; + use crate::triangulation::DelaunayTriangulation; + use crate::vertex; + + use slotmap::KeyData; + use std::cell::Cell; + use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; + + static DUPLICATE_DETECTION_FORCE_ENABLED: AtomicBool = AtomicBool::new(false); + + thread_local! { + static FORCE_NEXT_INSERTION_RETRYABLE_FAILURE: Cell = const { Cell::new(false) }; + } + + pub(super) fn duplicate_detection_force_enabled() -> bool { + DUPLICATE_DETECTION_FORCE_ENABLED.load(AtomicOrdering::Relaxed) + } + + fn set_duplicate_detection_force_enabled(enabled: bool) { + DUPLICATE_DETECTION_FORCE_ENABLED.store(enabled, AtomicOrdering::Relaxed); + } + + pub(super) fn take_force_next_insertion_retryable_failure() -> bool { + FORCE_NEXT_INSERTION_RETRYABLE_FAILURE.replace(false) + } + + fn set_force_next_insertion_retryable_failure(enabled: bool) -> bool { + FORCE_NEXT_INSERTION_RETRYABLE_FAILURE.replace(enabled) + } + + fn restore_force_next_insertion_retryable_failure(prior: bool) { + FORCE_NEXT_INSERTION_RETRYABLE_FAILURE.set(prior); + } + + fn insert( + tri: &mut Triangulation, + vertex: Vertex, + conflict_simplices: Option<&SimplexKeyBuffer>, + hint: Option, + ) -> Result<(VertexKey, Option), InsertionError> + where + K: Kernel, + K::Scalar: NumCast, + U: DataType, + V: DataType, + { + let (outcome, _stats) = insert_transactional( + tri, + vertex, + conflict_simplices, + hint, + DEFAULT_PERTURBATION_RETRIES, + 0, + None, + None, + )?; + match outcome { + InsertionOutcome::Inserted { vertex_key, hint } => Ok((vertex_key, hint)), + InsertionOutcome::Skipped { error } => Err(error), + } + } + + fn insert_with_statistics( + tri: &mut Triangulation, + vertex: Vertex, + conflict_simplices: Option<&SimplexKeyBuffer>, + hint: Option, + ) -> Result<(InsertionOutcome, InsertionStatistics), InsertionError> + where + K: Kernel, + K::Scalar: NumCast, + U: DataType, + V: DataType, + { + insert_transactional( + tri, + vertex, + conflict_simplices, + hint, + DEFAULT_PERTURBATION_RETRIES, + 0, + None, + None, + ) + } + + #[expect( + clippy::too_many_arguments, + reason = "Test helper mirrors the detailed transactional insertion signature" + )] + fn insert_transactional( + tri: &mut Triangulation, + vertex: Vertex, + conflict_simplices: Option<&SimplexKeyBuffer>, + hint: Option, + max_perturbation_attempts: usize, + perturbation_seed: u64, + index: Option<&mut HashGridIndex>, + bulk_index: Option, + ) -> Result<(InsertionOutcome, InsertionStatistics), InsertionError> + where + K: Kernel, + K::Scalar: NumCast, + U: DataType, + V: DataType, + { + let detail = tri.insert_transactional_detailed( + vertex, + conflict_simplices, + hint, + max_perturbation_attempts, + perturbation_seed, + index, + bulk_index, + InsertionTelemetryMode::CountsOnly, + )?; + Ok((detail.outcome, detail.stats)) + } + + struct ForceNextRetryableInsertionFailureGuard { + prior: bool, + } + + impl ForceNextRetryableInsertionFailureGuard { + fn enable() -> Self { + let prior = set_force_next_insertion_retryable_failure(true); + Self { prior } + } + } + + impl Drop for ForceNextRetryableInsertionFailureGuard { + fn drop(&mut self) { + restore_force_next_insertion_retryable_failure(self.prior); + } + } + + #[test] + fn test_retryable_conflict_trace_detail_formats_retryable_variants() { + let extra_simplex = SimplexKey::from(KeyData::from_ffi(10)); + let disconnected_simplex = SimplexKey::from(KeyData::from_ffi(11)); + let open_simplex = SimplexKey::from(KeyData::from_ffi(12)); + + let non_manifold = InsertionError::ConflictRegion(ConflictError::NonManifoldFacet { + facet_hash: 0xABCD, + simplex_count: 3, + }); + assert_eq!( + retryable_conflict_trace_detail(&non_manifold).as_deref(), + Some("kind=non_manifold_facet facet_hash=0xabcd simplex_count=3") + ); + + let ridge_fan = InsertionError::ConflictRegion(ConflictError::RidgeFan { + facet_count: 4, + ridge_vertex_count: 2, + extra_simplices: vec![extra_simplex], + }); + assert_eq!( + retryable_conflict_trace_detail(&ridge_fan).as_deref(), + Some("kind=ridge_fan facet_count=4 ridge_vertex_count=2 extra_simplices=1") + ); + + let disconnected = InsertionError::ConflictRegion(ConflictError::DisconnectedBoundary { + visited: 2, + total: 5, + disconnected_simplices: vec![disconnected_simplex], + }); + assert_eq!( + retryable_conflict_trace_detail(&disconnected).as_deref(), + Some("kind=disconnected_boundary visited=2 total=5 disconnected_simplices=1") + ); + + let open = InsertionError::ConflictRegion(ConflictError::OpenBoundary { + facet_count: 1, + ridge_vertex_count: 2, + open_simplex, + }); + assert_eq!( + retryable_conflict_trace_detail(&open).as_deref(), + Some("kind=open_boundary facet_count=1 ridge_vertex_count=2") + ); + + let not_retryable = InsertionError::CavityFilling { + reason: CavityFillingError::EmptyFanTriangulation, + }; + assert!(retryable_conflict_trace_detail(¬_retryable).is_none()); + } + + #[test] + fn test_cavity_conflict_error_summary_formats_all_variants() { + let simplex_key = SimplexKey::from(KeyData::from_ffi(21)); + + let cases = vec![ + ( + ConflictError::NonManifoldFacet { + facet_hash: 0xCAFE, + simplex_count: 4, + }, + "non_manifold_facet facet_hash=0xcafe simplex_count=4".to_string(), + ), + ( + ConflictError::RidgeFan { + facet_count: 5, + ridge_vertex_count: 3, + extra_simplices: vec![simplex_key], + }, + "ridge_fan facet_count=5 ridge_vertex_count=3 extra_simplices=1".to_string(), + ), + ( + ConflictError::DisconnectedBoundary { + visited: 1, + total: 3, + disconnected_simplices: vec![simplex_key], + }, + "disconnected_boundary visited=1 total=3 disconnected_simplices=1".to_string(), + ), + ( + ConflictError::OpenBoundary { + facet_count: 1, + ridge_vertex_count: 2, + open_simplex: simplex_key, + }, + format!( + "open_boundary facet_count=1 ridge_vertex_count=2 open_simplex={simplex_key:?}" + ), + ), + ( + ConflictError::InvalidStartSimplex { simplex_key }, + format!("invalid_start_simplex simplex_key={simplex_key:?}"), + ), + ( + ConflictError::SimplexDataAccessFailed { + simplex_key, + message: "missing vertices".to_string(), + }, + format!( + "simplex_data_access_failed simplex_key={simplex_key:?} message=missing vertices" + ), + ), + ]; + + for (error, expected) in cases { + assert_eq!(cavity_conflict_error_summary(&error), expected); + } + + let predicate = ConflictError::PredicateError { + source: CoordinateConversionError::ConversionFailed { + coordinate_index: 2, + coordinate_value: "NaN".to_string(), + from_type: "f64", + to_type: "f32", + }, + }; + assert!( + cavity_conflict_error_summary(&predicate) + .starts_with("predicate_error source=Failed to convert coordinate") + ); + + let internal = ConflictError::InternalInconsistency { + site: InternalInconsistencySite::RidgeInfoMissingSecondFacet { + first_facet: 4, + boundary_facets_len: 6, + ridge_vertex_count: 2, + }, + }; + assert!(cavity_conflict_error_summary(&internal).contains("internal_inconsistency site=")); + } + + #[test] + fn test_log_cavity_reduction_event_only_evaluates_when_enabled() { + let mut conflict_simplices = SimplexKeyBuffer::new(); + conflict_simplices.push(SimplexKey::from(KeyData::from_ffi(41))); + + let mut called = false; + log_cavity_reduction_event(false, 0, &conflict_simplices, || { + called = true; + "should not run".to_string() + }); + assert!(!called); + + log_cavity_reduction_event(true, 1, &conflict_simplices, || { + called = true; + "ran".to_string() + }); + assert!(called); + } + + #[test] + fn test_duplicate_detection_metrics_force_enable() { + struct DuplicateDetectionGuard; + + impl Drop for DuplicateDetectionGuard { + fn drop(&mut self) { + set_duplicate_detection_force_enabled(false); + } + } + + let _guard = DuplicateDetectionGuard; + set_duplicate_detection_force_enabled(true); + + let before = Triangulation::, (), (), 2>::duplicate_detection_metrics() + .expect("duplicate detection metrics should be enabled"); + + record_duplicate_detection_metrics(true, 3, false); + record_duplicate_detection_metrics(false, 0, true); + + let after = Triangulation::, (), (), 2>::duplicate_detection_metrics() + .expect("duplicate detection metrics should be enabled"); + + assert!(after.total_checks > before.total_checks); + assert!(after.grid_used > before.grid_used); + assert!(after.grid_fallbacks > before.grid_fallbacks); + assert!(after.grid_candidates >= before.grid_candidates + 3); + } + + fn unit_simplex_vertices() -> Vec> { + let mut vertices = Vec::with_capacity(D + 1); + vertices.push(vertex!([0.0_f64; D])); + for axis in 0..D { + let mut coords = [0.0_f64; D]; + coords[axis] = 1.0; + vertices.push(vertex!(coords)); + } + vertices + } + + /// Build a simplex whose feature length is controlled by one shared axis scale. + fn axis_scaled_simplex_vertices(scale: f64) -> Vec> { + let mut vertices = Vec::with_capacity(D + 1); + vertices.push(vertex!([0.0_f64; D])); + for axis in 0..D { + let mut coords = [0.0_f64; D]; + coords[axis] = scale; + vertices.push(vertex!(coords)); + } + vertices + } + + /// Build coordinates with only the first component set for tolerance-scale tests. + fn coords_with_first(first: f64) -> [f64; D] { + let mut coords = [0.0_f64; D]; + coords[0] = first; + coords + } + + #[test] + fn test_select_locate_hint_from_hash_grid_returns_incident_simplex() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let simplex_key = tri.tds.simplex_keys().next().unwrap(); + + let mut index: HashGridIndex = HashGridIndex::new(1.0); + for (vkey, vertex) in tri.tds.vertices() { + index.insert_vertex(vkey, vertex.point().coords()); + } + + let hint = tri.select_locate_hint_from_hash_grid(&[0.05, 0.05], &index); + assert_eq!(hint, Some(simplex_key)); + } + + #[test] + fn test_select_locate_hint_from_hash_grid_skips_missing_simplex() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + let vkey = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0])) + .unwrap(); + { + let vertex = tri.tds.vertex_mut(vkey).unwrap(); + vertex.set_incident_simplex(Some(SimplexKey::default())); + } + + let mut index: HashGridIndex = HashGridIndex::new(1.0); + index.insert_vertex(vkey, &[0.0, 0.0]); + + let hint = tri.select_locate_hint_from_hash_grid(&[0.0, 0.0], &index); + assert!(hint.is_none()); + } + + #[test] + fn test_duplicate_coordinates_error_uses_hash_grid_index() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + let vkey = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0])) + .unwrap(); + + let mut index: HashGridIndex = HashGridIndex::new(1.0); + index.insert_vertex(vkey, &[0.0, 0.0]); + + let tol = 1e-10_f64; + let err = tri.duplicate_coordinates_error(&[0.0, 0.0], tol, Some(&index)); + assert!(matches!( + err, + Some(InsertionError::DuplicateCoordinates { .. }) + )); + } + + #[test] + fn test_duplicate_coordinates_error_falls_back_when_index_unusable() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + let _ = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0])) + .unwrap(); + + let index: HashGridIndex = HashGridIndex::new(0.0); // unusable + let tol = 1e-10_f64; + let err = tri.duplicate_coordinates_error(&[0.0, 0.0], tol, Some(&index)); + assert!(matches!( + err, + Some(InsertionError::DuplicateCoordinates { .. }) + )); + } + + fn duplicate_coordinate_tolerance_scales_down_for_small_features() { + let mut tri: Triangulation, (), (), D> = + Triangulation::new_empty(FastKernel::new()); + let _ = tri + .tds + .insert_vertex_with_mapping(vertex!(coords_with_first::(1.0e-6))) + .unwrap(); + + let candidate = coords_with_first::(1.0e-6 + 1.0e-11); + let tolerance = tri.estimate_duplicate_coordinate_tolerance(&candidate, None); + + assert!( + tolerance < 1.0e-10, + "small-scale inputs should not inherit a fixed scalar-unit tolerance" + ); + assert!( + tri.duplicate_coordinates_error(&candidate, tolerance, None) + .is_none(), + "distinct small-scale vertices should not be skipped as duplicates" + ); + } + + fn duplicate_coordinate_tolerance_uses_hint_simplex_span() { + let vertices = unit_simplex_vertices::(); + let tds = + Triangulation::, (), (), D>::build_initial_simplex(&vertices).unwrap(); + let tri = Triangulation::, (), (), D>::new_with_tds(FastKernel::new(), tds); + let hint = tri.tds.simplex_keys().next(); + let candidate = coords_with_first::(5.0e-11); + let tolerance = tri.estimate_duplicate_coordinate_tolerance(&candidate, hint); + + assert!( + tolerance > 1.0e-10, + "unit-scale hint simplices should preserve near-duplicate filtering" + ); + assert!(matches!( + tri.duplicate_coordinates_error(&candidate, tolerance, None), + Some(InsertionError::DuplicateCoordinates { .. }) + )); + } + + fn duplicate_index_rebuilds_when_tolerance_exceeds_cell_size() { + let vertices = axis_scaled_simplex_vertices::(1.0e6); + let tds = + Triangulation::, (), (), D>::build_initial_simplex(&vertices).unwrap(); + let tri = Triangulation::, (), (), D>::new_with_tds(FastKernel::new(), tds); + let hint = tri.tds.simplex_keys().next(); + let candidate = [1.0_f64; D]; + let tolerance = tri.estimate_duplicate_coordinate_tolerance(&candidate, hint); + let mut index: HashGridIndex = HashGridIndex::new(1.0e-10); + for (vkey, vertex) in tri.tds.vertices() { + index.insert_vertex(vkey, vertex.point().coords()); + } + + tri.ensure_duplicate_index_cell_size(Some(&mut index), tolerance); + + approx::assert_abs_diff_eq!(index.cell_size(), tolerance, epsilon = f64::EPSILON); + assert!( + index.for_each_candidate_vertex_key(&candidate, |_| false), + "rebuilt duplicate index should remain queryable" + ); + } + + #[test] + fn test_duplicate_distance_within_tolerance_handles_overflowed_tolerance_square() { + assert!( + Triangulation::, (), (), 2>::duplicate_distance_within_tolerance( + f64::MAX, + f64::MAX + ) + ); + assert!( + !Triangulation::, (), (), 2>::duplicate_distance_within_tolerance( + f64::MAX, + 1.0 + ) + ); + } + + macro_rules! test_duplicate_tolerance_dimensions { + ($($dim:expr),+ $(,)?) => { + pastey::paste! { + $( + #[test] + fn []() { + duplicate_coordinate_tolerance_scales_down_for_small_features::<$dim>(); + } + + #[test] + fn []() { + duplicate_coordinate_tolerance_uses_hint_simplex_span::<$dim>(); + } + + #[test] + fn []() { + duplicate_index_rebuilds_when_tolerance_exceeds_cell_size::<$dim>(); + } + )+ + } + }; + } + + test_duplicate_tolerance_dimensions!(2, 3, 4, 5); + + #[test] + fn test_estimate_local_perturbation_scale_uses_hint_simplex_vertices() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let simplex_key = tri.tds.simplex_keys().next().unwrap(); + + let scale = tri.estimate_local_perturbation_scale(&[0.1, 0.0], Some(simplex_key)); + assert!((scale - 0.1).abs() < 1e-12); + } + + #[test] + fn test_estimate_local_perturbation_scale_clamps_to_min_scale() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + let _ = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0])) + .unwrap(); + + let scale = tri.estimate_local_perturbation_scale(&[0.0, 0.0], None); + let min_scale = ::default_tolerance(); + approx::assert_abs_diff_eq!(scale, min_scale, epsilon = f64::EPSILON); + } + + #[test] + fn test_insert_duplicate_coordinates_skips_with_statistics_and_errors_without() { + let mut tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + // First insertion succeeds. + insert(&mut tri, vertex!([0.0, 0.0, 0.0]), None, None) + .expect("first insertion should succeed"); + assert_eq!(tri.number_of_vertices(), 1); + + // Second insertion at same coordinates: insert() returns Err, insert_with_statistics() reports Skipped. + let err = insert(&mut tri, vertex!([0.0, 0.0, 0.0]), None, None).unwrap_err(); + assert!(matches!(err, InsertionError::DuplicateCoordinates { .. })); + + let (outcome, stats) = + insert_with_statistics(&mut tri, vertex!([0.0, 0.0, 0.0]), None, None).unwrap(); + assert!(stats.skipped()); + assert!(matches!(outcome, InsertionOutcome::Skipped { .. })); + + // No new vertex should have been inserted. + assert_eq!(tri.number_of_vertices(), 1); + } + + #[test] + fn test_insert_duplicate_uuid_is_non_retryable_and_rolls_back() { + // Insert a vertex, then attempt to insert another vertex with the same UUID. + let mut tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + insert(&mut tri, vertex!([0.0, 0.0, 0.0]), None, None) + .expect("first insertion should succeed"); + assert_eq!(tri.number_of_vertices(), 1); + + let existing_uuid = tri.tds.vertices().next().unwrap().1.uuid(); + let mut dup = vertex!([1.0, 0.0, 0.0]); + dup.set_uuid(existing_uuid).unwrap(); + + let err = insert(&mut tri, dup, None, None).unwrap_err(); + assert!( + !err.is_retryable(), + "Duplicate UUID should be non-retryable" + ); + + // Ensure rollback: vertex count unchanged. + assert_eq!(tri.number_of_vertices(), 1); + } + + // ============================================================================= + // Triangulation insert_with_statistics tests (internal API) + // ============================================================================= + + #[test] + fn triangulation_insert_with_statistics_basic_2d() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + // Insert first vertex + let (outcome, stats) = insert_with_statistics(&mut tri, vertex!([0.0, 0.0]), None, None) + .expect("insertion should succeed"); + + assert!(matches!( + outcome, + InsertionOutcome::Inserted { hint: None, .. } + )); + assert_eq!(stats.attempts, 1); + assert!(!stats.used_perturbation()); + assert!(!stats.skipped()); + assert!(stats.success()); + assert_eq!(tri.number_of_vertices(), 1); + } + + #[test] + fn triangulation_insert_with_statistics_bootstrap_3d() { + let mut tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + // Insert D+1 vertices to create initial simplex + 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]), + ]; + + for (i, v) in vertices.into_iter().enumerate() { + let (outcome, stats) = insert_with_statistics(&mut tri, v, None, None).unwrap(); + + assert!(matches!(outcome, InsertionOutcome::Inserted { .. })); + assert_eq!(stats.attempts, 1); + + if i < 3 { + // Bootstrap phase - no hint yet + assert!(matches!( + outcome, + InsertionOutcome::Inserted { hint: None, .. } + )); + } else { + // After D+1 vertices, hint should be available + assert!(matches!( + outcome, + InsertionOutcome::Inserted { hint: Some(_), .. } + )); + } + } + + assert_eq!(tri.number_of_vertices(), 4); + assert_eq!(tri.number_of_simplices(), 1); + } + + #[test] + fn triangulation_exterior_insert_3d_uses_local_conflict_without_global_scan() { + let mut tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + for coords in [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] { + insert_with_statistics(&mut tri, vertex!(coords), None, None).unwrap(); + } + + let hint = tri.simplices().next().map(|(simplex_key, _)| simplex_key); + let detail = tri + .insert_with_statistics_seeded_indexed_detailed( + vertex!([2.0, 2.0, 2.0]), + None, + hint, + 0, + None, + None, + ) + .unwrap(); + + assert!(matches!(detail.outcome, InsertionOutcome::Inserted { .. })); + assert_eq!(detail.telemetry.global_conflict_scans, 0); + assert_eq!(detail.telemetry.global_conflict_simplices_scanned, 0); + assert_eq!(detail.telemetry.global_conflict_simplices_found_total, 0); + assert_eq!(detail.telemetry.global_conflict_scan_nanos, 0); + assert_eq!(detail.telemetry.conflict_region_calls, 1); + assert_eq!(detail.telemetry.conflict_region_simplices_total, 0); + assert_eq!(detail.telemetry.cavity_insertion_calls, 0); + assert_eq!(detail.telemetry.hull_extension_calls, 1); + assert!( + !detail.repair_seed_simplices.is_empty(), + "hull extension should return local repair seeds" + ); + let facet_to_simplices = tri.tds.build_facet_to_simplices_map().unwrap(); + assert!( + facet_to_simplices + .values() + .all(|incident_simplices| incident_simplices.len() <= 2), + "hull extension should leave every facet with at most two incident simplices" + ); + assert!(tri.is_valid().is_ok()); + } + + #[test] + fn triangulation_exterior_insert_with_empty_conflicts_uses_local_repair_seeds() { + let mut tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + for coords in [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] { + insert_with_statistics(&mut tri, vertex!(coords), None, None).unwrap(); + } + + let hint = tri.simplices().next().map(|(simplex_key, _)| simplex_key); + let empty_conflicts = SimplexKeyBuffer::new(); + let detail = tri + .insert_with_statistics_seeded_indexed_detailed( + vertex!([2.0, 2.0, 2.0]), + Some(&empty_conflicts), + hint, + 0, + None, + None, + ) + .unwrap(); + + assert!(matches!(detail.outcome, InsertionOutcome::Inserted { .. })); + assert_eq!(detail.telemetry.global_conflict_scans, 0); + assert_eq!(detail.telemetry.conflict_region_calls, 1); + assert_eq!(detail.telemetry.conflict_region_simplices_total, 0); + assert_eq!(detail.telemetry.cavity_insertion_calls, 0); + assert_eq!(detail.telemetry.hull_extension_calls, 1); + assert!( + !detail.repair_seed_simplices.is_empty(), + "empty caller conflicts should still use terminal-simplex local repair seeds" + ); + let facet_to_simplices = tri.tds.build_facet_to_simplices_map().unwrap(); + assert!( + facet_to_simplices + .values() + .all(|incident_simplices| incident_simplices.len() <= 2), + "hull extension should leave every facet with at most two incident simplices" + ); + assert!(tri.is_valid().is_ok()); + } + + #[test] + fn triangulation_caller_conflicts_do_not_force_delaunay_repair() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + let start_simplex = tri + .simplices() + .next() + .map(|(simplex_key, _)| simplex_key) + .unwrap(); + let mut conflict_simplices = SimplexKeyBuffer::new(); + conflict_simplices.push(start_simplex); + let detail = tri + .insert_with_statistics_seeded_indexed_detailed( + vertex!([0.25, 0.25]), + Some(&conflict_simplices), + Some(start_simplex), + 0, + None, + None, + ) + .unwrap(); + + assert!(matches!(detail.outcome, InsertionOutcome::Inserted { .. })); + assert!( + !detail.delaunay_repair_required, + "caller-provided conflict simplices should preserve the cavity insertion repair flag" + ); + assert!(tri.is_valid().is_ok()); + } + + #[test] + fn triangulation_insert_with_statistics_hint_usage_4d() { + let mut tri: Triangulation, (), (), 4> = + Triangulation::new_empty(FastKernel::new()); + + // Build initial simplex + for i in 0..5 { + let mut coords = [0.0; 4]; + if i > 0 { + coords[i - 1] = 1.0; + } + insert_with_statistics(&mut tri, vertex!(coords), None, None).unwrap(); + } + + // Insert with explicit hint + let hint_simplex = tri.simplices().next().map(|(key, _)| key); + let (outcome, stats) = + insert_with_statistics(&mut tri, vertex!([0.2, 0.2, 0.2, 0.2]), None, hint_simplex) + .unwrap(); + + assert!(matches!( + outcome, + InsertionOutcome::Inserted { hint: Some(_), .. } + )); + assert_eq!(stats.attempts, 1); + assert!(stats.success()); + } + + #[test] + fn triangulation_insert_with_statistics_duplicate_coordinates_3d() { + let mut tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + // Insert first vertex + insert_with_statistics(&mut tri, vertex!([1.0, 2.0, 3.0]), None, None).unwrap(); + + // Try duplicate - should be skipped + let result = insert_with_statistics(&mut tri, vertex!([1.0, 2.0, 3.0]), None, None); + + assert!(matches!( + result, + Ok(( + InsertionOutcome::Skipped { + error: InsertionError::DuplicateCoordinates { .. } + }, + _ + )) + )); + } + + #[test] + fn triangulation_insert_with_statistics_multiple_insertions_2d() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + let points = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.5, 1.0]), + vertex!([0.3, 0.3]), + vertex!([0.7, 0.3]), + ]; + + let mut all_succeeded = true; + let mut max_attempts = 0; + + for point in points { + match insert_with_statistics(&mut tri, point, None, None) { + Ok((InsertionOutcome::Inserted { .. }, stats)) => { + max_attempts = max_attempts.max(stats.attempts); + assert!(stats.success()); + } + Ok((InsertionOutcome::Skipped { .. }, _)) | Err(_) => { + all_succeeded = false; + } + } + } + + assert!(all_succeeded, "all insertions should succeed"); + assert!(max_attempts >= 1); + assert_eq!(tri.number_of_vertices(), 5); + } + + #[test] + fn triangulation_insert_with_statistics_outcome_types() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + // Test Inserted variant + let (outcome, _) = + insert_with_statistics(&mut tri, vertex!([0.0, 0.0]), None, None).unwrap(); + + match outcome { + InsertionOutcome::Inserted { vertex_key, hint } => { + // Verify we can access the fields + assert!(tri.vertices().any(|(k, _)| k == vertex_key)); + assert_eq!(hint, None); // No hint during bootstrap + } + InsertionOutcome::Skipped { .. } => panic!("expected Inserted, got Skipped"), + } + } + + #[test] + fn triangulation_insert_with_statistics_sequential_5d() { + let mut tri: Triangulation, (), (), 5> = + Triangulation::new_empty(FastKernel::new()); + + // Insert 6 vertices to form initial simplex + for i in 0..6 { + let mut coords = [0.0; 5]; + if i > 0 { + coords[i - 1] = 1.0; + } + + let (outcome, stats) = + insert_with_statistics(&mut tri, vertex!(coords), None, None).unwrap(); + + assert!(matches!(outcome, InsertionOutcome::Inserted { .. })); + assert_eq!(stats.attempts, 1); + assert!(stats.success()); + } + + assert_eq!(tri.number_of_vertices(), 6); + assert_eq!(tri.number_of_simplices(), 1); + } + + #[test] + fn statistics_simplices_removed_during_repair() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + // Build simplex + insert_with_statistics(&mut tri, vertex!([0.0, 0.0]), None, None).unwrap(); + insert_with_statistics(&mut tri, vertex!([1.0, 0.0]), None, None).unwrap(); + insert_with_statistics(&mut tri, vertex!([0.5, 1.0]), None, None).unwrap(); + + let simplices_before = tri.number_of_simplices(); + + // Insert interior point - might trigger repair + let (_outcome, stats) = + insert_with_statistics(&mut tri, vertex!([0.5, 0.3]), None, None).unwrap(); + + let simplices_after = tri.number_of_simplices(); + + // Basic sanity: repair can't remove more simplices than existed before insertion. + assert!( + stats.simplices_removed_during_repair <= simplices_before, + "simplices_removed_during_repair ({}) should not exceed simplex count before insertion ({}); simplices after insertion: {}", + stats.simplices_removed_during_repair, + simplices_before, + simplices_after + ); + } + + // ============================================================================= + // insert_with_conflict_region: cavity reduction loop branch coverage + // + // These tests exercise `insert_with_conflict_region` directly via a synthetic + // TDS rather than through the public API. The goal is to cover the loop arms + // (RidgeFan SHRINK, DisconnectedBoundary EXPAND / SHRINK-fallback / else-break, + // and the post-loop error paths) that are not reachable through normal Delaunay + // insertions. + // ============================================================================= + + /// `DisconnectedBoundary` where disconnected simplices have no non-conflict neighbours: + /// `else { break; }` fires, then the D<3 star-split fallback is taken. + /// + /// Covers: `DisconnectedBoundary` `else { break; }` (line 3492), `should_fallback=true` + /// path (lines 3530-3555), and `suspicion.fallback_star_split` being set. + #[test] + fn test_cavity_reduction_disconnected_no_neighbors_sets_star_split_2d() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + // Two triangles that share no vertices (→ DisconnectedBoundary on extraction). + let v0 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0])) + .unwrap(); + let v1 = tri + .tds + .insert_vertex_with_mapping(vertex!([1.0, 0.0])) + .unwrap(); + let v2 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.5, 1.0])) + .unwrap(); + let v3 = tri + .tds + .insert_vertex_with_mapping(vertex!([5.0, 0.0])) + .unwrap(); + let v4 = tri + .tds + .insert_vertex_with_mapping(vertex!([6.0, 0.0])) + .unwrap(); + let v5 = tri + .tds + .insert_vertex_with_mapping(vertex!([5.5, 1.0])) + .unwrap(); + + let simplex_a = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) + .unwrap(); + let simplex_b = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![v3, v4, v5], None).unwrap()) + .unwrap(); + + // Neither simplex has any neighbour pointers, so `simplices_to_add` will be empty on + // the first iteration and the `else { break; }` arm fires immediately. + let new_v = tri + .tds + .insert_vertex_with_mapping(vertex!([0.5, 0.3])) + .unwrap(); + let point = Point::new([0.5_f64, 0.3_f64]); + + let mut conflict_simplices = SimplexKeyBuffer::new(); + conflict_simplices.push(simplex_a); + conflict_simplices.push(simplex_b); + + let mut suspicion = SuspicionFlags::default(); + let _ = tri.insert_with_conflict_region( + new_v, + &point, + conflict_simplices, + Some(simplex_a), + &mut suspicion, + ); + + // `else { break; }` → Err(DisconnectedBoundary) → should_fallback=true (D<3) + // → star-split fallback sets suspicion.fallback_star_split. + assert!( + suspicion.fallback_star_split, + "DisconnectedBoundary with no non-conflict neighbours should trigger star-split (D=2)" + ); + } + + /// Three 3D tetrahedra sharing the same triangular face → `NonManifoldFacet` on the + /// first extraction. D=3 → `should_fallback=false` → the function returns Err + /// immediately without entering the star-split path. + /// + /// Covers: `_ => break` (line 3511), `should_fallback=false` path (lines 3558-3563). + #[test] + fn test_cavity_reduction_nonmanifold_3d_returns_error_without_star_split() { + let mut tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + let v0 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) + .unwrap(); + let v1 = tri + .tds + .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) + .unwrap(); + let v2 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) + .unwrap(); + // Three distinct fourth vertices that all pair with the {v0,v1,v2} face. + let v3 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) + .unwrap(); + let v4 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, -1.0])) + .unwrap(); + let v5 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 2.0])) + .unwrap(); + + let simplex1 = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap()) + .unwrap(); + let simplex2 = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v4], None).unwrap()) + .unwrap(); + let simplex3 = tri + .tds + .insert_simplex_bypassing_topology_checks_for_test( + Simplex::new(vec![v0, v1, v2, v5], None).unwrap(), + ) + .unwrap(); + + let new_v = tri + .tds + .insert_vertex_with_mapping(vertex!([0.1, 0.1, 0.1])) + .unwrap(); + let point = Point::new([0.1_f64, 0.1_f64, 0.1_f64]); + + let mut conflict_simplices = SimplexKeyBuffer::new(); + conflict_simplices.push(simplex1); + conflict_simplices.push(simplex2); + conflict_simplices.push(simplex3); + + let mut suspicion = SuspicionFlags::default(); + let result = tri.insert_with_conflict_region( + new_v, + &point, + conflict_simplices, + None, + &mut suspicion, + ); + + // NonManifoldFacet → `_ => break` → should_fallback = D<3 = false → Err returned. + assert!(result.is_err(), "D=3 NonManifoldFacet should return Err"); + assert!( + !suspicion.fallback_star_split, + "D=3 should NOT enter star-split fallback" + ); + } + + /// Four 2D triangles all sharing a common vertex but with no shared edges produce a + /// `RidgeFan` error (`facet_count >= 3` for the shared vertex). Because + /// `conflict_simplices.len() = 4 > D+1 = 3`, the SHRINK branch fires on the first + /// iteration, removing the extra fan simplices from the conflict region. + /// + /// Covers: `RidgeFan` SHRINK body (lines 3434-3442) and re-extraction (line 3514). + #[test] + fn test_cavity_reduction_ridge_fan_shrink_fires_for_4_conflict_simplices_2d() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + // `center` appears in 8 boundary edges (2 per simplex × 4 simplices) → RidgeFan. + let center = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0])) + .unwrap(); + let va = tri + .tds + .insert_vertex_with_mapping(vertex!([-1.0, 2.0])) + .unwrap(); + let vb = tri + .tds + .insert_vertex_with_mapping(vertex!([1.0, 2.0])) + .unwrap(); + let vc = tri + .tds + .insert_vertex_with_mapping(vertex!([-3.0, -2.0])) + .unwrap(); + let vd = tri + .tds + .insert_vertex_with_mapping(vertex!([-2.0, -3.0])) + .unwrap(); + let ve = tri + .tds + .insert_vertex_with_mapping(vertex!([3.0, -2.0])) + .unwrap(); + let vf = tri + .tds + .insert_vertex_with_mapping(vertex!([2.0, -3.0])) + .unwrap(); + let vg = tri + .tds + .insert_vertex_with_mapping(vertex!([-4.0, 1.0])) + .unwrap(); + let vh = tri + .tds + .insert_vertex_with_mapping(vertex!([-4.0, -1.0])) + .unwrap(); + + let simplex1 = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![center, va, vb], None).unwrap()) + .unwrap(); + let simplex2 = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![center, vc, vd], None).unwrap()) + .unwrap(); + let simplex3 = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![center, ve, vf], None).unwrap()) + .unwrap(); + let simplex4 = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![center, vg, vh], None).unwrap()) + .unwrap(); + + let new_v = tri + .tds + .insert_vertex_with_mapping(vertex!([0.3, 1.0])) + .unwrap(); + let point = Point::new([0.3_f64, 1.0_f64]); + + let mut conflict_simplices = SimplexKeyBuffer::new(); + conflict_simplices.push(simplex1); + conflict_simplices.push(simplex2); + conflict_simplices.push(simplex3); + conflict_simplices.push(simplex4); + + let mut reduced_conflict_simplices = conflict_simplices.clone(); + let mut repair_seed_simplices = SimplexKeyBuffer::new(); + let mut delaunay_repair_required = false; + let _ = tri.reduce_conflict_region_to_cavity_boundary( + &mut reduced_conflict_simplices, + &mut repair_seed_simplices, + &mut delaunay_repair_required, + ); + assert!( + reduced_conflict_simplices.len() < conflict_simplices.len(), + "ridge-fan reduction should remove at least one extra simplex" + ); + assert!( + !repair_seed_simplices.is_empty(), + "removed ridge-fan simplices should seed later local repair" + ); + assert!( + delaunay_repair_required, + "shrinking an invalid cavity should force local Delaunay repair" + ); + + let mut suspicion = SuspicionFlags::default(); + // RidgeFan SHRINK fires on iteration 1 (4 > D+1=3), reducing conflict_simplices. + // The function completes without panic; result may be Ok or Err. + let _ = tri.insert_with_conflict_region( + new_v, + &point, + conflict_simplices, + Some(simplex1), + &mut suspicion, + ); + // Reaching here confirms the SHRINK branch executed successfully. + } + + /// Two completely disconnected 2D conflict simplices that each have one non-conflict + /// neighbour trigger the `DisconnectedBoundary` EXPAND path on the first iteration + /// (adding the neighbours), and the SHRINK-fallback on a subsequent iteration + /// (when `simplices_to_add` is empty but `conflict_simplices.len() > D+1`). + /// + /// Covers: EXPAND body (lines 3470-3480), SHRINK-fallback (lines 3481-3491), + /// and re-extraction after each reshape (line 3514). + #[test] + fn test_cavity_reduction_disconnected_expand_then_shrink_2d() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + // Group A: simplex_a = {v0,v1,v2} shares edge {v0,v1} with simplex_c = {v0,v1,v6}. + let v0 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0])) + .unwrap(); + let v1 = tri + .tds + .insert_vertex_with_mapping(vertex!([1.0, 0.0])) + .unwrap(); + let v2 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.5, 1.0])) + .unwrap(); + let v6 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.5, -1.0])) + .unwrap(); + // Group B: simplex_b = {v3,v4,v5} shares edge {v3,v4} with simplex_d = {v3,v4,v7}. + let v3 = tri + .tds + .insert_vertex_with_mapping(vertex!([5.0, 0.0])) + .unwrap(); + let v4 = tri + .tds + .insert_vertex_with_mapping(vertex!([6.0, 0.0])) + .unwrap(); + let v5 = tri + .tds + .insert_vertex_with_mapping(vertex!([5.5, 1.0])) + .unwrap(); + let v7 = tri + .tds + .insert_vertex_with_mapping(vertex!([5.5, -1.0])) + .unwrap(); + + let simplex_a = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) + .unwrap(); + // simplex_c is a non-conflict neighbour of simplex_a (not initially in conflict_simplices). + let simplex_c = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v6], None).unwrap()) + .unwrap(); + let simplex_b = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![v3, v4, v5], None).unwrap()) + .unwrap(); + // simplex_d is a non-conflict neighbour of simplex_b. + let simplex_d = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![v3, v4, v7], None).unwrap()) + .unwrap(); + + // Wire neighbours so EXPAND discovers simplex_c via simplex_a and simplex_d via simplex_b. + { + let simplex = tri.tds.simplex_mut(simplex_a).unwrap(); + simplex + .set_neighbors_from_keys([Some(simplex_c), None, None]) + .unwrap(); + } + { + let simplex = tri.tds.simplex_mut(simplex_b).unwrap(); + simplex + .set_neighbors_from_keys([Some(simplex_d), None, None]) + .unwrap(); + } + + let new_v = tri + .tds + .insert_vertex_with_mapping(vertex!([0.5, 0.3])) + .unwrap(); + let point = Point::new([0.5_f64, 0.3_f64]); + + let mut conflict_simplices = SimplexKeyBuffer::new(); + conflict_simplices.push(simplex_a); + conflict_simplices.push(simplex_b); + + // Iteration trace: + // 1. DisconnectedBoundary → EXPAND (adds simplex_c or simplex_d) → re-extract. + // 2. DisconnectedBoundary → EXPAND (adds the other) → re-extract. + // 3. DisconnectedBoundary, simplices_to_add=empty (all neighbours in conflict_set), + // len=4 > D+1=3 → SHRINK-fallback removes disconnected component → re-extract. + // 4. Two simplices sharing an edge → connected boundary → Ok → break. + let mut suspicion = SuspicionFlags::default(); + let _ = tri.insert_with_conflict_region( + new_v, + &point, + conflict_simplices, + Some(simplex_a), + &mut suspicion, + ); + // Reaching here without panic confirms EXPAND and SHRINK branches executed. + } + + #[test] + fn test_ensure_non_empty_conflict_simplices_passthrough_when_nonempty() { + let mut buf = SimplexKeyBuffer::new(); + buf.push(SimplexKey::from(KeyData::from_ffi(1))); + + let owned = Cow::Owned(buf); + let fallback = SimplexKey::from(KeyData::from_ffi(999)); + let result = + Triangulation::, (), (), 2>::ensure_non_empty_conflict_simplices( + owned, fallback, + ); + assert_eq!(result.len(), 1); + } + + #[test] + fn test_ensure_non_empty_conflict_simplices_uses_fallback_when_empty() { + let buf = SimplexKeyBuffer::new(); + let fallback = SimplexKey::from(KeyData::from_ffi(42)); + let result = + Triangulation::, (), (), 2>::ensure_non_empty_conflict_simplices( + Cow::Owned(buf), + fallback, + ); + assert_eq!(result.len(), 1); + assert_eq!(result[0], fallback); + } + + #[test] + fn test_star_split_boundary_facets_produces_d_plus_1_facets() { + let ck = SimplexKey::from(KeyData::from_ffi(7)); + let facets = Triangulation::, (), (), 3>::star_split_boundary_facets(ck); + assert_eq!(facets.len(), 4); // D+1 = 4 for 3D + for (i, fh) in facets.iter().enumerate() { + assert_eq!(fh.simplex_key(), ck); + assert_eq!(>::from(fh.facet_index()), i); + } + } + + // ========================================================================= + // INSERTION PIPELINE: BOOTSTRAP, INITIAL SIMPLEX, BEYOND-SIMPLEX + // ========================================================================= + + /// Helper: build a set of D+1 affinely independent vertices for dimension D. + fn simplex_vertices() -> Vec> { + let mut verts = Vec::with_capacity(D + 1); + // Origin + verts.push( + VertexBuilder::default() + .point(Point::new([0.0; D])) + .build() + .unwrap(), + ); + // Unit vectors along each axis + for i in 0..D { + let mut coords = [0.0; D]; + coords[i] = 1.0; + verts.push( + VertexBuilder::default() + .point(Point::new(coords)) + .build() + .unwrap(), + ); + } + verts + } + + /// Macro: dimension-parametric insertion pipeline tests. + macro_rules! test_insert_pipeline { + ($dim:literal) => { + pastey::paste! { + #[test] + fn []() { + let mut tri: Triangulation, (), (), $dim> = + Triangulation::new_empty(FastKernel::new()); + + // Insert fewer than D+1 vertices: should remain in bootstrap (no simplices). + let verts = simplex_vertices::<$dim>(); + for v in &verts[..$dim] { + let (vk, hint) = insert(&mut tri, *v, None, None).unwrap(); + assert!(hint.is_none(), "{}D: no hint during bootstrap", $dim); + assert!(tri.tds.vertex(vk).is_some()); + } + assert_eq!(tri.number_of_vertices(), $dim); + assert_eq!(tri.number_of_simplices(), 0, "{}D: no simplices during bootstrap", $dim); + } + + #[test] + fn []() { + let mut tri: Triangulation, (), (), $dim> = + Triangulation::new_empty(FastKernel::new()); + + let verts = simplex_vertices::<$dim>(); + for v in &verts { + insert(&mut tri, *v, None, None).unwrap(); + } + assert_eq!(tri.number_of_vertices(), $dim + 1); + assert_eq!(tri.number_of_simplices(), 1, "{}D: exactly 1 simplex after D+1 vertices", $dim); + + // The simplex must have D+1 vertices. + let (_, simplex) = tri.simplices().next().unwrap(); + assert_eq!(simplex.number_of_vertices(), $dim + 1); + } + + #[test] + fn []() { + let mut tri: Triangulation, (), (), $dim> = + Triangulation::new_empty(FastKernel::new()); + + // Build initial simplex. + let verts = simplex_vertices::<$dim>(); + for v in &verts { + insert(&mut tri, *v, None, None).unwrap(); + } + assert_eq!(tri.number_of_simplices(), 1); + + // Insert an interior point. + let mut interior = [0.0; $dim]; + for c in interior.iter_mut() { + *c = 1.0 / (>::from($dim + 1) * 2.0); + } + let interior_vertex = VertexBuilder::default() + .point(Point::new(interior)) + .build() + .unwrap(); + let (_, hint) = insert(&mut tri, interior_vertex, None, None).unwrap(); + + assert!(hint.is_some(), "{}D: hint returned after D+2 insertion", $dim); + assert!(tri.number_of_simplices() > 1, "{}D: simplex count increased", $dim); + assert!(tri.is_valid().is_ok(), "{}D: topology valid after insertion", $dim); + } + } + }; + } + + test_insert_pipeline!(2); + test_insert_pipeline!(3); + test_insert_pipeline!(4); + + // ========================================================================= + // INSERT_WITH_STATISTICS: STATISTICS TRACKING + // ========================================================================= + + #[test] + fn test_insert_with_statistics_tracks_simplices_removed() { + // Build a 2D triangulation with several points, verify stats fields. + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + let points = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.25, 0.25]]; + for coords in &points { + let (outcome, stats) = + insert_with_statistics(&mut tri, vertex!(*coords), None, None).unwrap(); + assert!(stats.success()); + assert!(matches!(outcome, InsertionOutcome::Inserted { .. })); + assert_eq!(stats.attempts, 1); + } + assert_eq!(tri.number_of_vertices(), 4); + assert!(tri.number_of_simplices() >= 2); + } + + // ========================================================================= + // DUPLICATE COORDINATES ERROR: LINEAR FALLBACK (NO INDEX) + // ========================================================================= + + #[test] + fn test_duplicate_coordinates_error_linear_scan_no_index() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + let _ = tri + .tds + .insert_vertex_with_mapping(vertex!([3.0, 4.0])) + .unwrap(); + + let tol = 1e-10_f64; + // No index provided: should fall back to linear scan. + let err = tri.duplicate_coordinates_error(&[3.0, 4.0], tol, None); + assert!(matches!( + err, + Some(InsertionError::DuplicateCoordinates { .. }) + )); + + // Non-duplicate should return None. + let no_err = tri.duplicate_coordinates_error(&[99.0, 99.0], tol, None); + assert!(no_err.is_none()); + } + + // ========================================================================= + // ESTIMATE LOCAL PERTURBATION SCALE: NO VERTICES + // ========================================================================= + + #[test] + fn test_estimate_local_perturbation_scale_no_vertices() { + let tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + let scale = tri.estimate_local_perturbation_scale(&[1.0, 2.0, 3.0], None); + // With no vertices, scale should be 1.0 (the default). + approx::assert_abs_diff_eq!(scale, 1.0, epsilon = 1e-12); + } + + // ========================================================================= + // PROGRESSIVE PERTURBATION: SCALE INVARIANCE + // ========================================================================= + + /// Construct the same 3D geometry at three different uniform scales and verify + /// that the same number of vertices are successfully inserted at each scale. + /// This validates that perturbation is proportional to local feature size. + #[test] + fn test_perturbation_scale_invariance_3d() { + const EXPECTED_VERTEX_COUNT: usize = 8; + const EXPECTED_SIMPLEX_COUNT: usize = 10; + + fn build_at_scale(scale: f64) -> (usize, usize) { + let base_coords: [[f64; 3]; 8] = [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [1.0, 1.0, 0.0], + [1.0, 0.0, 1.0], + [0.0, 1.0, 1.0], + [0.5, 0.5, 0.5], + ]; + let vertices: Vec> = base_coords + .iter() + .map(|c| vertex!([c[0] * scale, c[1] * scale, c[2] * scale])) + .collect(); + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + (dt.number_of_vertices(), dt.number_of_simplices()) + } + + let (v1, c1) = build_at_scale(1.0); + let (v2, c2) = build_at_scale(1e6); + let (v3, c3) = build_at_scale(1e-6); + + // Absolute expectations: catch regressions that affect all scales equally. + assert_eq!( + v1, EXPECTED_VERTEX_COUNT, + "Vertex count regression at unit scale (build_at_scale(1.0))" + ); + assert_eq!( + c1, EXPECTED_SIMPLEX_COUNT, + "Simplex count regression at unit scale (build_at_scale(1.0))" + ); + + // Cross-scale equality: perturbation is proportional to local feature size. + assert_eq!( + v1, v2, + "Vertex count should be scale-invariant (×1 vs ×1e6)" + ); + assert_eq!( + v1, v3, + "Vertex count should be scale-invariant (×1 vs ×1e-6)" + ); + assert_eq!( + c1, c2, + "Simplex count should be scale-invariant (×1 vs ×1e6)" + ); + assert_eq!( + c1, c3, + "Simplex count should be scale-invariant (×1 vs ×1e-6)" + ); + } + + /// Verify the mantissa-based epsilon selection (`1e-4` for f32, `1e-8` for f64) + /// and exercise the perturbation retry path with a near-degenerate simplex. + #[test] + fn test_perturbation_epsilon_selection_and_retry() { + // Assert the mantissa-digits → epsilon branching for each scalar type. + // insert_transactional uses: `if K::Scalar::mantissa_digits() <= 24 { 1e-4 } else { 1e-8 }` + assert_eq!( + f32::mantissa_digits(), + 24, + "f32 should take the 1e-4 epsilon path" + ); + assert_eq!( + f64::mantissa_digits(), + 53, + "f64 should take the 1e-8 epsilon path" + ); + + // f32 path: build a 2D triangulation, then insert a point exactly on an + // existing edge. This near-degenerate configuration exercises the full + // insert_transactional path including epsilon_value computation. + let initial_f32: Vec> = vec![ + vertex!([0.0_f32, 0.0]), + vertex!([1.0_f32, 0.0]), + vertex!([0.0_f32, 1.0]), + ]; + let tds_f32 = + Triangulation::, (), (), 2>::build_initial_simplex(&initial_f32) + .unwrap(); + let mut tri_f32 = Triangulation::, (), (), 2>::new_with_tds( + AdaptiveKernel::::new(), + tds_f32, + ); + + // Point on edge [0,0]→[1,0]: collinear, exercises degeneracy handling. + let (outcome_f32, stats_f32) = + insert_with_statistics(&mut tri_f32, vertex!([0.5_f32, 0.0]), None, None).unwrap(); + // Should succeed (SoS resolves) or be gracefully skipped. + assert!( + stats_f32.attempts >= 1, + "f32 insertion must execute at least 1 attempt" + ); + if let InsertionOutcome::Inserted { .. } = outcome_f32 { + assert_eq!(tri_f32.tds.number_of_vertices(), 4); + } + + // f64 path: same exercise with double precision. + let initial_f64: Vec> = vec![ + vertex!([0.0_f64, 0.0]), + vertex!([1.0_f64, 0.0]), + vertex!([0.0_f64, 1.0]), + ]; + let tds_f64 = + Triangulation::, (), (), 2>::build_initial_simplex(&initial_f64) + .unwrap(); + let mut tri_f64 = Triangulation::, (), (), 2>::new_with_tds( + AdaptiveKernel::::new(), + tds_f64, + ); + + let (outcome_f64, stats_f64) = + insert_with_statistics(&mut tri_f64, vertex!([0.5_f64, 0.0]), None, None).unwrap(); + assert!( + stats_f64.attempts >= 1, + "f64 insertion must execute at least 1 attempt" + ); + if let InsertionOutcome::Inserted { .. } = outcome_f64 { + assert_eq!(tri_f64.tds.number_of_vertices(), 4); + } + } + + /// Verify the `DEFAULT_PERTURBATION_RETRIES` constant value. + #[test] + fn test_default_perturbation_retries_constant() { + assert_eq!( + DEFAULT_PERTURBATION_RETRIES, 3, + "Default perturbation retries should be 3 (4 total attempts)" + ); + } + + // ========================================================================= + // PROGRESSIVE PERTURBATION: RETRY PATH COVERAGE + // ========================================================================= + + #[expect( + clippy::too_many_lines, + reason = "Literal 4D repro point set keeps retry-path coverage deterministic" + )] + fn perturbation_retry_repro_points_4d() -> [Point; 20] { + // Fixed adversarial insertion sequence captured from the former + // randomized sweep (seed 4, index 19). The final insertion exhausts + // perturbation retries in the current 4D path, so this keeps retry + // coverage deterministic without looping over random seeds. + [ + Point::new([ + 0.660_063_804_566_304_3, + 3.139_352_812_821_116, + 1.460_437_437_858_557_2, + 1.683_976_950_416_514_7, + ]), + Point::new([ + 2.451_966_162_957_145, + 9.547_229_335_697_903, + 3.306_128_696_560_687_5, + -3.722_166_730_957_705_6, + ]), + Point::new([ + -2.344_360_378_074_79, + -2.755_831_029_562_339, + -1.275_699_073_649_171_6, + 7.667_812_493_160_508, + ]), + Point::new([ + -8.633_692_230_033_44, + 1.995_093_685_275_964_6, + 7.993_316_108_703_105, + -3.310_780_098_197_376_7, + ]), + Point::new([ + 9.710_410_828_147_591, + -9.675_293_457_452_888, + -7.169_080_272_753_141, + 5.405_946_111_675_925_5, + ]), + Point::new([ + 2.266_246_031_487_613, + 2.481_673_939_102_995, + 3.039_413_140_674_462, + 4.441_464_307_622_285, + ]), + Point::new([ + 2.565_731_492_709_954, + 8.916_218_617_699_3, + -3.878_340_784_199_263_4, + -9.518_720_806_139_726, + ]), + Point::new([ + -2.067_801_258_479_087_2, + -5.739_002_626_992_522, + 7.554_154_642_458_165, + -2.983_334_995_469_171_2, + ]), + Point::new([ + 7.592_645_474_686_005, + -3.326_646_745_715_216, + -3.259_537_116_123_248, + -4.935_000_398_073_641, + ]), + Point::new([ + -5.931_807_896_262_18, + 8.897_268_005_841_394, + 0.324_049_126_782_281_15, + -8.328_532_028_712_647, + ]), + Point::new([ + -8.182_644_118_410_867, + 5.373_925_359_941_506, + -9.015_837_749_827_128, + -1.703_973_344_007_208, + ]), + Point::new([ + 1.455_467_619_488_706_2, + 9.869_985_381_801_74, + 8.605_618_759_378_327, + -1.050_236_122_559_873_3, + ]), + Point::new([ + -5.687_160_826_499_058, + 6.504_655_423_433_022, + 8.941_590_411_569_816, + 9.543_547_641_077_382, + ]), + Point::new([ + 8.975_549_245_653_312, + -8.089_655_037_805_944, + 9.936_284_142_216_682, + -7.816_992_427_475_977, + ]), + Point::new([ + 5.825_845_324_524_742, + -7.639_141_597_632_388, + 1.549_524_653_880_336_4, + 4.563_088_344_949_309, + ]), + Point::new([ + 7.387_141_055_690_918, + 6.194_972_387_680_284, + -5.764_015_058_796_046, + 9.298_338_336_238_999, + ]), + Point::new([ + -1.597_916_740_077_209_9, + -4.938_008_036_006_716, + 7.414_979_546_687_874, + -7.718_146_418_588_452, + ]), + Point::new([ + -2.414_045_007_912_424_3, + 8.888_648_260_600_007, + -5.859_329_894_512_815, + 3.268_096_825_406_147, + ]), + Point::new([ + -8.294_250_893_230_837, + 3.083_275_278_154_95, + 8.020_989_920_767_69, + 8.155_291_219_012_977, + ]), + Point::new([ + 6.718_748_825_685_814_6, + -4.640_634_945_941_695, + 2.283_644_483_657_752_7, + 0.837_537_687_473_188_8, + ]), + ] + } + + /// Exercise both successful perturbation retry (`attempt > 0`) and + /// exhaustion (`SkippedDegeneracy`) paths with deterministic 4D fixtures. + /// + /// Covers: progressive scale factor, perturbation coordinate generation + /// with `perturbation_seed == 0`, retry decision, retry success, and + /// retry exhaustion. + #[test] + fn test_perturbation_retry_and_exhaustion_4d() { + let initial_vertices: Vec> = vec![ + vertex!([0.0, 0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0, 0.0]), + vertex!([0.0, 1.0, 0.0, 0.0]), + vertex!([0.0, 0.0, 1.0, 0.0]), + vertex!([0.0, 0.0, 0.0, 1.0]), + ]; + let tds = Triangulation::, (), (), 4>::build_initial_simplex( + &initial_vertices, + ) + .unwrap(); + let mut retry_success_tri = Triangulation::, (), (), 4>::new_with_tds( + AdaptiveKernel::new(), + tds, + ); + + let _guard = ForceNextRetryableInsertionFailureGuard::enable(); + let retry_success_vertex = VertexBuilder::default() + .point(Point::new([0.2, 0.2, 0.2, 0.2])) + .build() + .unwrap(); + let (_outcome, retry_success_stats) = + insert_with_statistics(&mut retry_success_tri, retry_success_vertex, None, None) + .unwrap(); + let saw_retry = retry_success_stats.used_perturbation() && retry_success_stats.success(); + + let mut exhaustion_tri: Triangulation, (), (), 4> = + Triangulation::new_empty(AdaptiveKernel::new()); + let mut saw_exhausted_skip = false; + + for point in perturbation_retry_repro_points_4d() { + let v = VertexBuilder::default().point(point).build().unwrap(); + let (outcome, stats) = + insert_with_statistics(&mut exhaustion_tri, v, None, None).unwrap(); + + saw_exhausted_skip |= stats.skipped() + && stats.attempts == DEFAULT_PERTURBATION_RETRIES + 1 + && matches!(stats.result, InsertionResult::SkippedDegeneracy) + && matches!(outcome, InsertionOutcome::Skipped { error } if error.is_retryable()); + } + + assert!( + saw_retry, + "deterministic 4D fixture did not trigger a successful perturbation retry" + ); + assert!( + saw_exhausted_skip, + "deterministic 4D adversarial repro did not trigger retry exhaustion" + ); + } + + /// Exercise the seeded perturbation branch (`perturbation_seed != 0`) + /// by calling `insert_transactional` directly. + /// + /// Covers: the `mix` computation and sign selection in the seeded path + /// (lines using `perturbation_seed ^ ...`). + /// + /// Uses the same deterministic 4D repro as + /// [`test_perturbation_retry_and_exhaustion_4d`]. + #[test] + fn test_perturbation_retry_seeded_branch_4d() { + let mut tri: Triangulation, (), (), 4> = + Triangulation::new_empty(AdaptiveKernel::new()); + + for point in perturbation_retry_repro_points_4d() { + let v = VertexBuilder::default().point(point).build().unwrap(); + let (_outcome, stats) = insert_transactional( + &mut tri, + v, + None, + None, + DEFAULT_PERTURBATION_RETRIES, + 0xDEAD_BEEF, + None, + None, + ) + .unwrap(); + + if stats.used_perturbation() && (stats.success() || stats.skipped()) { + return; + } + } + + panic!("deterministic 4D adversarial repro did not trigger the seeded perturbation branch"); + } +} diff --git a/src/core/operations.rs b/src/core/operations.rs index 1d87d778..382656a7 100644 --- a/src/core/operations.rs +++ b/src/core/operations.rs @@ -11,8 +11,8 @@ use crate::core::algorithms::incremental_insertion::InsertionError; use crate::core::tds::SimplexKey; -use crate::core::triangulation::TopologyGuarantee; -use crate::triangulation::delaunay::{DelaunayCheckPolicy, DelaunayRepairPolicy}; +use crate::core::validation::TopologyGuarantee; +use crate::repair::{DelaunayCheckPolicy, DelaunayRepairPolicy}; /// Semantic classification of topological modifications to a triangulation. /// @@ -22,8 +22,8 @@ use crate::triangulation::delaunay::{DelaunayCheckPolicy, DelaunayRepairPolicy}; /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::operations::TopologicalOperation; -/// use delaunay::prelude::triangulation::TopologyGuarantee; +/// use delaunay::prelude::operations::TopologicalOperation; +/// use delaunay::prelude::TopologyGuarantee; /// /// let op = TopologicalOperation::FacetFlip; /// assert!(op.is_admissible_under(TopologyGuarantee::Pseudomanifold)); @@ -45,8 +45,8 @@ pub enum TopologicalOperation { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::operations::{RepairDecision, RepairSkipReason, TopologicalOperation}; -/// use delaunay::prelude::triangulation::TopologyGuarantee; +/// use delaunay::prelude::operations::{RepairDecision, RepairSkipReason, TopologicalOperation}; +/// use delaunay::prelude::TopologyGuarantee; /// /// let decision = RepairDecision::Skip { /// reason: RepairSkipReason::Inadmissible { @@ -73,7 +73,7 @@ pub enum RepairDecision { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::operations::RepairSkipReason; +/// use delaunay::prelude::operations::RepairSkipReason; /// /// let reason = RepairSkipReason::PolicyDisabled; /// assert!(matches!(reason, RepairSkipReason::PolicyDisabled)); @@ -159,7 +159,7 @@ impl TopologicalOperation { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::operations::InsertionResult; +/// use delaunay::prelude::operations::InsertionResult; /// /// let result = InsertionResult::default(); /// assert_eq!(result, InsertionResult::Inserted); @@ -180,7 +180,7 @@ pub enum InsertionResult { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::operations::{InsertionResult, InsertionStatistics}; +/// use delaunay::prelude::operations::{InsertionResult, InsertionStatistics}; /// /// let stats = InsertionStatistics { /// attempts: 2, @@ -362,8 +362,8 @@ impl DelaunayInsertionState { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::insertion::InsertionError; -/// use delaunay::prelude::triangulation::operations::InsertionOutcome; +/// use delaunay::prelude::insertion::InsertionError; +/// use delaunay::prelude::operations::InsertionOutcome; /// /// let outcome = InsertionOutcome::Skipped { /// error: InsertionError::DuplicateCoordinates { @@ -401,7 +401,7 @@ pub enum InsertionOutcome { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::operations::SuspicionFlags; +/// use delaunay::prelude::operations::SuspicionFlags; /// /// let flags = SuspicionFlags { /// perturbation_used: true, diff --git a/src/core/orientation.rs b/src/core/orientation.rs new file mode 100644 index 00000000..f48111c5 --- /dev/null +++ b/src/core/orientation.rs @@ -0,0 +1,781 @@ +//! Geometric orientation orchestration for generic [`Triangulation`](crate::Triangulation). +//! +//! This module owns triangulation-level orientation work: collecting lifted simplex +//! points from the TDS, validating geometric orientation, canonicalizing simplex +//! slot order, and normalizing coherent orientation after construction or edits. +//! Predicate implementations remain in the geometry layer. + +use crate::core::algorithms::incremental_insertion::InsertionError; +use crate::core::collections::{MAX_PRACTICAL_DIMENSION_SIZE, SimplexKeyBuffer, SmallBuffer}; +use crate::core::simplex::Simplex; +use crate::core::tds::{GeometricError, SimplexKey, TdsError, VertexKey}; +use crate::core::triangulation::Triangulation; +use crate::geometry::kernel::Kernel; +use crate::geometry::point::Point; +use crate::geometry::predicates::Orientation; +use crate::geometry::robust_predicates::robust_orientation; +use crate::geometry::traits::coordinate::Coordinate; +use crate::topology::traits::global_topology_model::GlobalTopologyModel; + +impl Triangulation +where + K: Kernel, +{ + /// Collect simplex points for orientation evaluation. + /// + /// For periodic simplices, this delegates per-vertex lattice-offset lifting to the active + /// [`GlobalTopology`](crate::topology::traits::topological_space::GlobalTopology) behavior model. + fn collect_simplex_points_for_orientation( + &self, + simplex_key: SimplexKey, + simplex: &Simplex, + purpose: &str, + ) -> Result, MAX_PRACTICAL_DIMENSION_SIZE>, TdsError> { + let topology_model = self.global_topology.model(); + let periodic_offsets = simplex.periodic_vertex_offsets(); + if let Some(offsets) = periodic_offsets { + if offsets.len() != simplex.number_of_vertices() { + return Err(TdsError::DimensionMismatch { + expected: simplex.number_of_vertices(), + actual: offsets.len(), + context: format!( + "simplex {:?} (key {simplex_key:?}) periodic offset count vs vertex count during {purpose}", + simplex.uuid(), + ), + }); + } + if !topology_model.supports_periodic_orientation_offsets() { + return Err(TdsError::InconsistentDataStructure { + message: format!( + "Simplex {:?} (key {simplex_key:?}) has periodic offsets (count {}) during {purpose}, but triangulation global topology is {:?} (kind {:?}, allows_boundary: {}, periodic_domain: {:?}); expected periodic-orientation-offset-capable topology", + simplex.uuid(), + offsets.len(), + self.global_topology, + topology_model.kind(), + topology_model.allows_boundary(), + topology_model.periodic_domain(), + ), + }); + } + } + + let mut points: SmallBuffer, MAX_PRACTICAL_DIMENSION_SIZE> = + SmallBuffer::with_capacity(simplex.number_of_vertices()); + + for (vertex_idx, &vertex_key) in simplex.vertices().iter().enumerate() { + let vertex = self.tds.vertex(vertex_key).ok_or_else(|| { + TdsError::VertexNotFound { + vertex_key, + context: format!( + "referenced by simplex {:?} (key {simplex_key:?}) at position {vertex_idx} during {purpose}", + simplex.uuid(), + ), + } + })?; + let periodic_offset = periodic_offsets.map(|offsets| offsets[vertex_idx]); + let lifted_coords = topology_model + .lift_for_orientation(*vertex.point().coords(), periodic_offset) + .map_err(|error| TdsError::InconsistentDataStructure { + message: format!( + "Failed to lift coordinates for vertex key {vertex_key:?} at slot {vertex_idx} in simplex {:?} (key {simplex_key:?}) during {purpose}: {error}", + simplex.uuid(), + ), + })?; + + points.push(Point::new(lifted_coords)); + } + + Ok(points) + } + + /// Evaluate a simplex's geometric orientation for a validation/canonicalization context. + /// + /// This helper uses [`robust_orientation`] directly, without `SoS`, so true + /// degeneracy remains distinguishable from positive or negative orientation. + pub(crate) fn evaluate_simplex_orientation_for_context( + &self, + simplex_key: SimplexKey, + simplex: &Simplex, + purpose: &str, + predicate_failure_prefix: &str, + ) -> Result { + let points = self.collect_simplex_points_for_orientation(simplex_key, simplex, purpose)?; + + match robust_orientation(&points) { + Ok(Orientation::POSITIVE) => Ok(1), + Ok(Orientation::NEGATIVE) => Ok(-1), + Ok(Orientation::DEGENERATE) => Ok(0), + Err(error) => Err(TdsError::InconsistentDataStructure { + message: format!( + "{predicate_failure_prefix} {:?} (key {simplex_key:?}): {error}", + simplex.uuid(), + ), + }), + } + } + + /// Validates geometric orientation sign for each stored simplex using exact arithmetic. + /// + /// Simplices are stored in canonical positive orientation order by construction and mutation + /// paths; a negative sign indicates geometric/combinatorial mismatch. + pub(crate) fn validate_geometric_simplex_orientation(&self) -> Result<(), TdsError> { + for (simplex_key, simplex) in self.tds.simplices() { + let orientation = self.evaluate_simplex_orientation_for_context( + simplex_key, + simplex, + "geometric orientation validation", + "Geometric orientation predicate failed for simplex", + )?; + if orientation < 0 { + let vertex_keys: SmallBuffer = + simplex.vertices().iter().copied().collect(); + let neighbor_keys: SmallBuffer, MAX_PRACTICAL_DIMENSION_SIZE> = + simplex + .neighbor_keys() + .map(Iterator::collect) + .unwrap_or_default(); + tracing::debug!( + simplex_uuid = %simplex.uuid(), + ?simplex_key, + ?vertex_keys, + ?neighbor_keys, + orientation, + "negative geometric orientation detected during validation", + ); + + return Err(TdsError::Geometric(GeometricError::NegativeOrientation { + message: format!( + "Simplex {:?} (key {simplex_key:?}, vertices {vertex_keys:?}) has negative geometric orientation; expected positive canonical orientation", + simplex.uuid(), + ), + })); + } + } + + Ok(()) + } + + /// Validates geometric orientation for a local set of simplices. + pub(crate) fn validate_geometric_simplex_orientation_for_simplices( + &self, + simplices: &[SimplexKey], + ) -> Result<(), TdsError> { + for &simplex_key in simplices { + let simplex = + self.tds + .simplex(simplex_key) + .ok_or_else(|| TdsError::SimplexNotFound { + simplex_key, + context: "local geometric orientation validation scope".to_string(), + })?; + let orientation = self.evaluate_simplex_orientation_for_context( + simplex_key, + simplex, + "local geometric orientation validation", + "Geometric orientation predicate failed for local simplex", + )?; + if orientation < 0 { + let vertex_keys: SmallBuffer = + simplex.vertices().iter().copied().collect(); + tracing::debug!( + simplex_uuid = %simplex.uuid(), + ?simplex_key, + ?vertex_keys, + orientation, + "negative geometric orientation detected during local validation", + ); + + return Err(TdsError::Geometric(GeometricError::NegativeOrientation { + message: format!( + "Simplex {:?} (key {simplex_key:?}, vertices {vertex_keys:?}) has negative geometric orientation; expected positive canonical orientation", + simplex.uuid(), + ), + })); + } + } + + Ok(()) + } + + /// Validates local orientation invariants for simplices changed by insertion. + pub(crate) fn validate_local_orientation_for_simplices( + &self, + simplices: &[SimplexKey], + ) -> Result<(), InsertionError> { + self.tds + .validate_coherent_orientation_for_simplices(simplices)?; + self.validate_geometric_simplex_orientation_for_simplices(simplices)?; + Ok(()) + } + + /// Flip all negatively oriented simplices to positive orientation. + fn promote_simplices_to_positive_orientation(&mut self) -> Result { + let mut negative_simplices = SimplexKeyBuffer::new(); + + for (simplex_key, simplex) in self.tds.simplices() { + let orientation = self.evaluate_simplex_orientation_for_context( + simplex_key, + simplex, + "positive-orientation promotion", + "Geometric orientation predicate failed while promoting positive orientation for simplex", + )?; + if orientation == 0 { + continue; + } + if orientation < 0 { + negative_simplices.push(simplex_key); + } + } + + if negative_simplices.is_empty() { + return Ok(false); + } + + for simplex_key in negative_simplices { + let simplex = + self.tds + .simplex_mut(simplex_key) + .ok_or_else(|| TdsError::SimplexNotFound { + simplex_key, + context: "applying positive-orientation promotion".to_string(), + })?; + if simplex.number_of_vertices() >= 2 { + simplex.swap_vertex_slots(0, 1); + } + } + + self.tds.mark_topology_modified(); + Ok(true) + } + + /// Check whether any simplex still requires positive-orientation promotion. + fn simplices_require_positive_orientation_promotion(&self) -> Result { + for (simplex_key, simplex) in self.tds.simplices() { + let orientation = self.evaluate_simplex_orientation_for_context( + simplex_key, + simplex, + "positive-orientation convergence check", + "Geometric orientation predicate failed while checking positive-orientation convergence for simplex", + )?; + if orientation == 0 { + continue; + } + if orientation < 0 { + return Ok(true); + } + } + + Ok(false) + } + + /// For connected non-periodic triangulations, canonicalize the coherent global sign. + fn canonicalize_global_orientation_sign(&mut self) -> Result<(), InsertionError> { + let representative_sign = { + let mut sign = None; + for (simplex_key, simplex) in self.tds.simplices() { + let orientation = self.evaluate_simplex_orientation_for_context( + simplex_key, + simplex, + "global orientation-sign canonicalization", + "Geometric orientation predicate failed while canonicalizing global orientation sign for simplex", + )?; + if orientation != 0 { + sign = Some(orientation); + break; + } + } + sign + }; + + if representative_sign != Some(-1) { + return Ok(()); + } + + let simplex_keys: SimplexKeyBuffer = self.tds.simplex_keys().collect(); + let mut flipped_any = false; + for simplex_key in simplex_keys { + let Some(simplex) = self.tds.simplex_mut(simplex_key) else { + continue; + }; + if simplex.number_of_vertices() >= 2 { + simplex.swap_vertex_slots(0, 1); + flipped_any = true; + } + } + + if flipped_any { + self.tds.mark_topology_modified(); + } + + Ok(()) + } + + /// Normalize coherent orientation and promote geometric orientation to the positive sign. + pub(crate) fn normalize_and_promote_positive_orientation( + &mut self, + ) -> Result<(), InsertionError> { + self.tds.normalize_coherent_orientation()?; + self.canonicalize_global_orientation_sign()?; + + for _ in 0..3 { + if !self.promote_simplices_to_positive_orientation()? { + break; + } + self.tds.normalize_coherent_orientation()?; + } + + if self.simplices_require_positive_orientation_promotion()? { + let mut residual_count = 0_usize; + let mut sample_keys: [Option; 5] = [None; 5]; + for (simplex_key, simplex) in self.tds.simplices() { + let orientation = self.evaluate_simplex_orientation_for_context( + simplex_key, + simplex, + "residual negative-orientation sampling", + "Geometric orientation predicate failed while sampling residual negatives for simplex", + )?; + if orientation < 0 { + if residual_count < sample_keys.len() { + sample_keys[residual_count] = Some(simplex_key); + } + residual_count += 1; + } + } + let sampled: Vec = sample_keys.into_iter().flatten().collect(); + tracing::debug!( + residual_count, + sampled_keys = ?sampled, + "normalize_and_promote_positive_orientation: \ + {residual_count} simplices still appear negative after bounded promotion \ + passes (likely near-degenerate FP noise); accepting coherent orientation" + ); + } + self.canonicalize_global_orientation_sign()?; + Ok(()) + } + + /// Canonicalize a set of newly created simplices to positive geometric orientation. + #[expect( + clippy::too_many_lines, + reason = "debug-only orientation diagnostics with dedup add conditional branches" + )] + pub(crate) fn canonicalize_positive_orientation_for_simplices( + &mut self, + simplices: &SimplexKeyBuffer, + ) -> Result<(), InsertionError> { + #[cfg(debug_assertions)] + let debug_orientation = std::env::var_os("DELAUNAY_DEBUG_ORIENTATION").is_some(); + #[cfg(debug_assertions)] + let mut orientation_warn_count = 0_usize; + + for &simplex_key in simplices { + let orientation = { + let simplex = + self.tds + .simplex(simplex_key) + .ok_or_else(|| TdsError::SimplexNotFound { + simplex_key, + context: "canonicalizing insertion orientation".to_string(), + })?; + self.evaluate_simplex_orientation_for_context( + simplex_key, + simplex, + "insertion orientation canonicalization", + "Geometric orientation predicate failed while canonicalizing simplex", + )? + }; + + if orientation == 0 { + continue; + } + + if orientation < 0 { + #[cfg(debug_assertions)] + let pre_swap_vertices = if debug_orientation { + self.tds.simplex(simplex_key).map(|c| c.vertices().to_vec()) + } else { + None + }; + + let simplex = + self.tds + .simplex_mut(simplex_key) + .ok_or_else(|| TdsError::SimplexNotFound { + simplex_key, + context: "applying insertion orientation canonicalization".to_string(), + })?; + if simplex.number_of_vertices() < 2 { + return Err(TdsError::DimensionMismatch { + expected: 2, + actual: simplex.number_of_vertices(), + context: format!( + "simplex {simplex_key:?} needs >= 2 vertices for orientation canonicalization" + ), + } + .into()); + } + simplex.swap_vertex_slots(0, 1); + + #[cfg(debug_assertions)] + if debug_orientation { + orientation_warn_count += 1; + if orientation_warn_count <= 3 { + let post_orientation = self.tds.simplex(simplex_key).map(|c| { + self.evaluate_simplex_orientation_for_context( + simplex_key, + c, + "orientation swap verification", + "orientation predicate failed during swap verification", + ) + }); + match post_orientation { + Some(Ok(post_o)) => { + tracing::warn!( + simplex_key = ?simplex_key, + pre_swap_vertices = ?pre_swap_vertices, + pre_swap_orientation = orientation, + post_swap_orientation = post_o, + swap_fixed = post_o > 0, + "canonicalize_positive_orientation: negative-orientation simplex swapped" + ); + } + Some(Err(ref e)) => { + tracing::warn!( + simplex_key = ?simplex_key, + pre_swap_vertices = ?pre_swap_vertices, + pre_swap_orientation = orientation, + error = %e, + "canonicalize_positive_orientation: post-swap verification failed" + ); + } + None => { + tracing::warn!( + simplex_key = ?simplex_key, + pre_swap_vertices = ?pre_swap_vertices, + pre_swap_orientation = orientation, + "canonicalize_positive_orientation: simplex not found after swap" + ); + } + } + } + } + } + } + + #[cfg(debug_assertions)] + if orientation_warn_count > 3 && debug_orientation { + let suppressed = orientation_warn_count - 3; + tracing::warn!( + total_negative = orientation_warn_count, + suppressed, + "canonicalize_positive_orientation: suppressed {suppressed} additional negative-orientation warnings (see first 3 above)" + ); + } + + Ok(()) + } + + /// Verifies that no simplex is geometrically degenerate. + /// + /// This is a sign-agnostic check: it flags simplices whose exact orientation + /// determinant is zero regardless of the sign. + pub(crate) fn validate_geometric_nondegeneracy(&self) -> Result<(), TdsError> { + for (simplex_key, simplex) in self.tds.simplices() { + let orientation = self.evaluate_simplex_orientation_for_context( + simplex_key, + simplex, + "geometric nondegeneracy check", + "Orientation predicate failed for simplex", + )?; + if orientation == 0 { + return Err(TdsError::Geometric(GeometricError::DegenerateOrientation { + message: format!( + "Simplex {:?} (key {simplex_key:?}) is geometrically degenerate \ + (zero-volume simplex from collinear/coplanar vertices)", + simplex.uuid(), + ), + })); + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::tds::InvariantError; + use crate::geometry::kernel::FastKernel; + use crate::topology::traits::topological_space::{GlobalTopology, ToroidalConstructionMode}; + use crate::vertex; + + /// Regression test: a negatively oriented but topologically valid simplex + /// passes topology-only validation while failing full validation. + #[test] + fn negative_oriented_simplex_topology_only() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + assert!(tri.is_valid().is_ok()); + assert!(tri.is_valid_topology_only().is_ok()); + + let simplex_key = tri.tds.simplex_keys().next().unwrap(); + tri.tds + .simplex_mut(simplex_key) + .unwrap() + .swap_vertex_slots(0, 1); + + assert!(tri.is_valid_topology_only().is_ok()); + assert!(tri.is_valid().is_err()); + } + + #[test] + fn local_geometric_orientation_validation_errors_on_missing_scope_simplex() { + 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 tds = + Triangulation::, (), (), 3>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); + let simplex_key = tri.tds.simplex_keys().next().unwrap(); + assert_eq!(tri.tds.remove_simplices_by_keys(&[simplex_key]), 1); + + match tri.validate_geometric_simplex_orientation_for_simplices(&[simplex_key]) { + Err(TdsError::SimplexNotFound { + simplex_key: missing_key, + .. + }) => assert_eq!(missing_key, simplex_key), + other => panic!("Expected SimplexNotFound, got {other:?}"), + } + } + + #[test] + fn is_valid_rejects_negative_geometric_simplex_orientation() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + + let simplex_key = tds.simplex_keys().next().unwrap(); + tds.simplex_mut(simplex_key) + .unwrap() + .swap_vertex_slots(0, 1); + + let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let err = tri.is_valid().unwrap_err(); + assert!(matches!( + err, + InvariantError::Tds(TdsError::Geometric(GeometricError::NegativeOrientation { message })) + if message.contains("negative geometric orientation") + )); + } + + #[test] + fn validate_geometric_simplex_orientation_returns_enriched_error_on_negative() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + + let simplex_key = tds.simplex_keys().next().unwrap(); + tds.simplex_mut(simplex_key) + .unwrap() + .swap_vertex_slots(0, 1); + + let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let err = tri.validate_geometric_simplex_orientation().unwrap_err(); + assert!( + matches!( + &err, + TdsError::Geometric(GeometricError::NegativeOrientation { message }) + if message.contains("negative geometric orientation") + && message.contains("vertices") + ), + "Error should contain vertex keys: {err}" + ); + } + + #[test] + fn simplices_require_positive_orientation_promotion_detects_negative_without_mutating() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let simplex_key = tds.simplex_keys().next().unwrap(); + tds.simplex_mut(simplex_key) + .unwrap() + .swap_vertex_slots(0, 1); + + let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let before: Vec<_> = tri.tds.simplex(simplex_key).unwrap().vertices().to_vec(); + + assert!( + tri.simplices_require_positive_orientation_promotion() + .unwrap() + ); + + let after: Vec<_> = tri.tds.simplex(simplex_key).unwrap().vertices().to_vec(); + assert_eq!(before, after); + } + + #[test] + fn simplices_require_positive_orientation_promotion_false_for_positive_without_mutating() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let simplex_key = tds.simplex_keys().next().unwrap(); + + let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let before: Vec<_> = tri.tds.simplex(simplex_key).unwrap().vertices().to_vec(); + + assert!( + !tri.simplices_require_positive_orientation_promotion() + .unwrap() + ); + + let after: Vec<_> = tri.tds.simplex(simplex_key).unwrap().vertices().to_vec(); + assert_eq!(before, after); + } + + #[test] + fn periodic_geometric_orientation_validation_uses_lifted_coordinates() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([0.8, 0.0]), + vertex!([0.0, 0.8]), + ]; + let mut tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let simplex_key = tds.simplex_keys().next().unwrap(); + tds.simplex_mut(simplex_key) + .unwrap() + .set_periodic_vertex_offsets(vec![[0, 0], [0, 0], [1, 0]]) + .unwrap(); + + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + tri.set_global_topology(GlobalTopology::Toroidal { + domain: [1.0, 1.0], + mode: ToroidalConstructionMode::PeriodicImagePoint, + }); + + assert!(tri.validate_geometric_simplex_orientation().is_ok()); + + tri.tds + .simplex_mut(simplex_key) + .unwrap() + .swap_vertex_slots(0, 1); + let err = tri.validate_geometric_simplex_orientation().unwrap_err(); + assert!(matches!( + err, + TdsError::Geometric(GeometricError::NegativeOrientation { message }) + if message.contains("negative geometric orientation") + )); + } + + #[test] + fn periodic_geometric_orientation_validation_requires_toroidal_metadata() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([0.8, 0.0]), + vertex!([0.0, 0.8]), + ]; + let mut tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let simplex_key = tds.simplex_keys().next().unwrap(); + tds.simplex_mut(simplex_key) + .unwrap() + .set_periodic_vertex_offsets(vec![[0, 0], [0, 0], [1, 0]]) + .unwrap(); + + let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let err = tri.validate_geometric_simplex_orientation().unwrap_err(); + assert!(matches!( + err, + TdsError::InconsistentDataStructure { message } + if message.contains("has periodic offsets") + && message.contains("expected periodic-orientation-offset-capable topology") + )); + } + + #[test] + fn periodic_geometric_orientation_validation_rejects_offset_count_mismatch() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([0.8, 0.0]), + vertex!([0.0, 0.8]), + ]; + let mut tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let simplex_key = tds.simplex_keys().next().unwrap(); + tds.simplex_mut(simplex_key) + .unwrap() + .periodic_vertex_offsets = Some(vec![[0, 0], [1, 0]].into()); + + let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let err = tri.validate_geometric_simplex_orientation().unwrap_err(); + assert!(matches!( + err, + TdsError::DimensionMismatch { + expected: 3, + actual: 2, + .. + } + )); + } + + #[test] + fn periodic_geometric_orientation_validation_maps_lift_errors() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([0.8, 0.0]), + vertex!([0.0, 0.8]), + ]; + let mut tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let simplex_key = tds.simplex_keys().next().unwrap(); + tds.simplex_mut(simplex_key) + .unwrap() + .set_periodic_vertex_offsets(vec![[0, 0], [0, 0], [1, 0]]) + .unwrap(); + + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + tri.set_global_topology(GlobalTopology::Toroidal { + domain: [0.0, 1.0], + mode: ToroidalConstructionMode::PeriodicImagePoint, + }); + + let err = tri.validate_geometric_simplex_orientation().unwrap_err(); + assert!(matches!( + err, + TdsError::InconsistentDataStructure { message } + if message.contains("Failed to lift coordinates") + && message.contains("Invalid toroidal period") + )); + } +} diff --git a/src/core/query.rs b/src/core/query.rs new file mode 100644 index 00000000..eeb59df1 --- /dev/null +++ b/src/core/query.rs @@ -0,0 +1,1182 @@ +//! Read-only queries for generic [`Triangulation`](crate::Triangulation). +//! +//! This module owns zero-mutation accessors and topology traversal helpers for +//! the generic triangulation layer. Mutation APIs stay with the construction and +//! editing modules; validation orchestration stays in [`crate::core::validation`]. + +use crate::core::adjacency::{AdjacencyIndex, AdjacencyIndexBuildError}; +use crate::core::collections::{ + FastHashMap, FastHashSet, MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer, VertexToSimplicesMap, + fast_hash_map_with_capacity, fast_hash_set_with_capacity, +}; +use crate::core::edge::EdgeKey; +use crate::core::facet::{AllFacetsIter, BoundaryFacetsIter}; +use crate::core::simplex::Simplex; +use crate::core::tds::{SimplexKey, VertexKey}; +use crate::core::triangulation::Triangulation; +use crate::core::vertex::Vertex; +use crate::geometry::kernel::Kernel; +#[cfg(debug_assertions)] +use std::sync::atomic::{AtomicU64, Ordering}; + +#[cfg(debug_assertions)] +static VERTEX_TO_SIMPLICES_SPILL_EVENTS: AtomicU64 = AtomicU64::new(0); + +impl Triangulation +where + K: Kernel, +{ + /// Returns an iterator over all simplices in the triangulation. + /// + /// Delegates to the underlying Tds. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); + /// let tri = dt.as_triangulation(); + /// + /// // Iterate over simplices + /// for (_simplex_key, simplex) in tri.simplices() { + /// assert_eq!(simplex.number_of_vertices(), 3); // 2D triangle + /// } + /// assert_eq!(tri.simplices().count(), 1); + /// ``` + pub fn simplices(&self) -> impl Iterator)> { + self.tds.simplices() + } + + /// Returns an iterator over all vertices in the triangulation. + /// + /// Delegates to the underlying Tds. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); + /// let tri = dt.as_triangulation(); + /// + /// // Iterate over vertices + /// for (_vertex_key, vertex) in tri.vertices() { + /// assert_eq!(vertex.dim(), 2); // 2D vertices + /// } + /// assert_eq!(tri.vertices().count(), 3); + /// ``` + pub fn vertices(&self) -> impl Iterator)> { + self.tds.vertices() + } + + /// Returns the number of vertices in the triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// 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::new(&vertices).unwrap(); + /// assert_eq!(dt.as_triangulation().number_of_vertices(), 4); + /// ``` + #[must_use] + pub fn number_of_vertices(&self) -> usize { + self.tds.number_of_vertices() + } + + /// Returns the number of simplices in the triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// 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::new(&vertices).unwrap(); + /// assert_eq!(dt.as_triangulation().number_of_simplices(), 1); // Single tetrahedron + /// ``` + #[must_use] + pub fn number_of_simplices(&self) -> usize { + self.tds.number_of_simplices() + } + + /// Returns the dimension of the triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::geometry::*; + /// use delaunay::prelude::*; + /// + /// // 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![ + /// 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::new(&vertices).unwrap(); + /// assert_eq!(dt.as_triangulation().dim(), 3); + /// ``` + #[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 facet is represented as a lightweight `FacetView` that references the + /// underlying triangulation data. + /// + /// # Returns + /// + /// An iterator yielding `FacetView` objects for all facets in the triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// 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::new(&vertices).unwrap(); + /// + /// // Iterate over all facets + /// let facet_count = dt.as_triangulation().facets().count(); + /// assert_eq!(facet_count, 4); // Tetrahedron has 4 facets + /// ``` + pub fn facets(&self) -> AllFacetsIter<'_, K::Scalar, U, V, D> { + AllFacetsIter::new(&self.tds) + } + + /// Returns an iterator over boundary (hull) facets in the triangulation. + /// + /// Boundary facets are those that belong to exactly one simplex. This method + /// computes the facet-to-simplices map internally for convenience. + /// + /// # Returns + /// + /// An iterator yielding `FacetView` objects for boundary facets only. + /// + /// # Panics + /// + /// Panics if the triangulation data structure is corrupted (simplices have invalid + /// neighbor relationships or facet information). This indicates a bug in the + /// library and should never happen with a properly constructed triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// 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::new(&vertices).unwrap(); + /// + /// let boundary_count = dt.as_triangulation().boundary_facets().count(); + /// assert_eq!(boundary_count, 4); // All facets are on boundary + /// ``` + pub fn boundary_facets(&self) -> BoundaryFacetsIter<'_, K::Scalar, U, V, D> { + // build_facet_to_simplices_map only fails if simplices have invalid structure, + // which should never happen in a valid triangulation + let facet_map = self + .tds + .build_facet_to_simplices_map() + .expect("Failed to build facet map - triangulation structure is corrupted"); + BoundaryFacetsIter::new(&self.tds, facet_map) + } + + #[inline] + fn debug_assert_adjacency_index_matches(&self, index: &AdjacencyIndex) { + // AdjacencyIndex is built from a snapshot of a triangulation. We cannot enforce at + // compile-time that an index belongs to this triangulation, but we can cheaply catch + // obvious mix-ups in debug builds. + debug_assert_eq!( + index.vertex_to_simplices.len(), + self.tds.number_of_vertices(), + "AdjacencyIndex vertex_to_simplices size does not match triangulation vertex count" + ); + debug_assert_eq!( + index.vertex_to_edges.len(), + self.tds.number_of_vertices(), + "AdjacencyIndex vertex_to_edges size does not match triangulation vertex count" + ); + } + + /// Returns an iterator over all unique edges in the triangulation. + /// + /// Edges are inferred from the vertex lists of each simplex; they are not stored explicitly. + /// + /// ## Allocation and iteration order + /// + /// This method allocates an internal set to deduplicate edges. The iteration order is + /// not specified. + /// + /// If you need fast repeated topology queries, consider building an + /// [`AdjacencyIndex`] once via [`Triangulation::build_adjacency_index`](Self::build_adjacency_index). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// // A single 3D tetrahedron has 6 unique edges. + /// 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::new(&vertices).unwrap(); + /// let tri = dt.as_triangulation(); + /// + /// let edges: std::collections::HashSet<_> = tri.edges().collect(); + /// assert_eq!(edges.len(), 6); + /// ``` + pub fn edges(&self) -> impl Iterator + '_ { + self.collect_edges().into_iter() + } + + /// Returns an iterator over all unique edges using a precomputed [`AdjacencyIndex`]. + /// + /// This avoids per-call deduplication and allocations. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// // A single 3D tetrahedron has 6 unique edges. + /// 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::new(&vertices).unwrap(); + /// let tri = dt.as_triangulation(); + /// + /// let index = tri.build_adjacency_index().unwrap(); + /// let edges: std::collections::HashSet<_> = tri.edges_with_index(&index).collect(); + /// assert_eq!(edges.len(), 6); + /// ``` + pub fn edges_with_index<'a>( + &self, + index: &'a AdjacencyIndex, + ) -> impl Iterator + 'a { + self.debug_assert_adjacency_index_matches(index); + index.edges() + } + + /// Returns the number of unique edges in the triangulation. + /// + /// This is equivalent to `self.edges().count()`. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// // A single 2D triangle has 3 unique edges. + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let tri = dt.as_triangulation(); + /// + /// assert_eq!(tri.number_of_edges(), 3); + /// ``` + #[must_use] + pub fn number_of_edges(&self) -> usize { + self.collect_edges().len() + } + + /// Returns the number of unique edges using a precomputed [`AdjacencyIndex`]. + /// + /// This is equivalent to `self.edges_with_index(index).count()`. + /// + /// # Examples + /// + /// ```rust + /// # use delaunay::prelude::query::*; + /// # 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::new(&vertices).unwrap(); + /// # let tri = dt.as_triangulation(); + /// # let index = tri.build_adjacency_index().unwrap(); + /// assert_eq!(tri.number_of_edges_with_index(&index), 6); + /// ``` + #[must_use] + pub fn number_of_edges_with_index(&self, index: &AdjacencyIndex) -> usize { + self.debug_assert_adjacency_index_matches(index); + index.number_of_edges() + } + + /// Returns an iterator over all simplices adjacent (incident) to a vertex. + /// + /// If `v` is not present in this triangulation, the iterator is empty. + /// + /// Iteration order is not specified. + pub fn adjacent_simplices(&self, v: VertexKey) -> impl Iterator + '_ { + self.tds + .find_simplices_containing_vertex_by_key(v) + .into_iter() + } + + /// Returns an iterator over all simplices adjacent (incident) to a vertex using a precomputed + /// [`AdjacencyIndex`]. + pub fn adjacent_simplices_with_index<'a>( + &self, + index: &'a AdjacencyIndex, + v: VertexKey, + ) -> impl Iterator + 'a { + self.debug_assert_adjacency_index_matches(index); + index.adjacent_simplices(v) + } + + /// Returns the number of simplices adjacent (incident) to a vertex using a precomputed + /// [`AdjacencyIndex`]. + #[must_use] + pub fn number_of_adjacent_simplices_with_index( + &self, + index: &AdjacencyIndex, + v: VertexKey, + ) -> usize { + self.debug_assert_adjacency_index_matches(index); + index.number_of_adjacent_simplices(v) + } + + /// Returns an iterator over all neighbors of a simplex. + /// + /// Boundary facets are omitted (only existing neighbors are yielded). If `c` is not + /// present, the iterator is empty. + /// + /// Iteration order is not specified. + pub fn simplex_neighbors(&self, c: SimplexKey) -> impl Iterator + '_ { + self.tds + .simplex(c) + .and_then(Simplex::neighbors) + .into_iter() + .flat_map(IntoIterator::into_iter) + .flatten() + .filter(|&neighbor_key| self.tds.contains_simplex(neighbor_key)) + } + + /// Returns an iterator over all neighbors of a simplex using a precomputed [`AdjacencyIndex`]. + pub fn simplex_neighbors_with_index<'a>( + &self, + index: &'a AdjacencyIndex, + c: SimplexKey, + ) -> impl Iterator + 'a { + self.debug_assert_adjacency_index_matches(index); + index.simplex_neighbors(c) + } + + /// Returns the number of neighbors of a simplex using a precomputed [`AdjacencyIndex`]. + #[must_use] + pub fn number_of_simplex_neighbors_with_index( + &self, + index: &AdjacencyIndex, + c: SimplexKey, + ) -> usize { + self.debug_assert_adjacency_index_matches(index); + index.number_of_simplex_neighbors(c) + } + + /// Returns an iterator over all unique edges incident to a vertex. + /// + /// If `v` is not present in this triangulation, the iterator is empty. + pub fn incident_edges(&self, v: VertexKey) -> impl Iterator + '_ { + self.collect_incident_edges(v).into_iter() + } + + /// Returns an iterator over all unique edges incident to a vertex using a precomputed + /// [`AdjacencyIndex`]. + pub fn incident_edges_with_index<'a>( + &self, + index: &'a AdjacencyIndex, + v: VertexKey, + ) -> impl Iterator + 'a { + self.debug_assert_adjacency_index_matches(index); + index.incident_edges(v) + } + + /// Returns the number of unique edges incident to a vertex using a precomputed + /// [`AdjacencyIndex`]. + #[must_use] + pub fn number_of_incident_edges_with_index( + &self, + index: &AdjacencyIndex, + v: VertexKey, + ) -> usize { + self.debug_assert_adjacency_index_matches(index); + index.number_of_incident_edges(v) + } + + /// Returns the number of unique edges incident to a vertex. + /// + /// If `v` is not present in this triangulation, returns 0. + #[must_use] + pub fn number_of_incident_edges(&self, v: VertexKey) -> usize { + self.collect_incident_edges(v).len() + } + + /// Returns a slice view of a simplex's vertex keys. + /// + /// This is a zero-allocation accessor. If `c` is not present, returns `None`. + #[must_use] + pub fn simplex_vertices(&self, c: SimplexKey) -> Option<&[VertexKey]> { + self.tds.simplex(c).map(Simplex::vertices) + } + + /// Returns a slice view of a vertex's coordinates. + /// + /// This is a zero-allocation accessor. If `v` is not present, returns `None`. + #[must_use] + pub fn vertex_coords(&self, v: VertexKey) -> Option<&[K::Scalar]> { + self.tds + .vertex(v) + .map(|vertex| &vertex.point().coords()[..]) + } + + /// Builds an immutable adjacency index for fast repeated topology queries. + /// + /// This never stores any cache internally and does not mutate the triangulation. + /// + /// ## Notes + /// + /// - No sorted-order guarantees are provided for the values. + /// - The returned collections are optimized for performance. + /// - The maps include an entry for every vertex currently stored in the triangulation. + /// During the bootstrap phase (before the initial simplex is created), vertices have empty + /// adjacency lists because no simplices exist yet. This is expected and not an error condition. + /// - Isolated vertices (present in the vertex store but not referenced by any simplex) are allowed at + /// the TDS structural layer, but violate the Level 3 manifold invariants checked by + /// [`Triangulation::is_valid`](Self::is_valid). When present, their adjacency lists are empty. + /// + /// # Errors + /// + /// Returns an error if the triangulation data structure is internally inconsistent + /// (e.g., a simplex references a missing vertex key or a missing neighbor simplex key). + pub fn build_adjacency_index(&self) -> Result { + let vertex_cap = self.tds.number_of_vertices(); + let simplex_cap = self.tds.number_of_simplices(); + + let mut vertex_to_simplices: VertexToSimplicesMap = fast_hash_map_with_capacity(vertex_cap); + let mut simplex_to_neighbors: FastHashMap< + SimplexKey, + SmallBuffer, + > = fast_hash_map_with_capacity(simplex_cap); + let mut vertex_to_edges: FastHashMap< + VertexKey, + SmallBuffer, + > = fast_hash_map_with_capacity(vertex_cap); + + let edges_per_simplex = (D + 1).saturating_mul(D) / 2; + let mut seen_edges: FastHashSet = + fast_hash_set_with_capacity(simplex_cap.saturating_mul(edges_per_simplex)); + + for (simplex_key, simplex) in self.tds.simplices() { + let vertices = simplex.vertices(); + + for &vk in vertices { + if !self.tds.contains_vertex_key(vk) { + return Err(AdjacencyIndexBuildError::MissingVertexKey { + simplex_key, + vertex_key: vk, + }); + } + let entry = vertex_to_simplices.entry(vk).or_default(); + #[cfg(debug_assertions)] + let was_spilled = entry.spilled(); + entry.push(simplex_key); + #[cfg(debug_assertions)] + if !was_spilled && entry.spilled() { + let spill_count = + VERTEX_TO_SIMPLICES_SPILL_EVENTS.fetch_add(1, Ordering::Relaxed) + 1; + tracing::debug!( + "VertexToSimplicesMap spill #{spill_count}: vertex={vk:?} len={} cap={} (MAX_PRACTICAL_DIMENSION_SIZE={MAX_PRACTICAL_DIMENSION_SIZE})", + entry.len(), + entry.capacity() + ); + } + } + + if let Some(neighbors) = simplex.neighbor_keys() { + let mut neighs: SmallBuffer = + SmallBuffer::new(); + + for n_opt in neighbors { + let Some(nk) = n_opt else { + continue; + }; + + if !self.tds.contains_simplex(nk) { + return Err(AdjacencyIndexBuildError::MissingNeighborSimplex { + simplex_key, + neighbor_key: nk, + }); + } + + neighs.push(nk); + } + + if !neighs.is_empty() { + simplex_to_neighbors.insert(simplex_key, neighs); + } + } + + for i in 0..vertices.len() { + for j in (i + 1)..vertices.len() { + let edge = EdgeKey::new(vertices[i], vertices[j]); + if !seen_edges.insert(edge) { + continue; + } + + let (a, b) = edge.endpoints(); + vertex_to_edges.entry(a).or_default().push(edge); + vertex_to_edges.entry(b).or_default().push(edge); + } + } + } + + for (vk, _) in self.tds.vertices() { + vertex_to_simplices.entry(vk).or_default(); + vertex_to_edges.entry(vk).or_default(); + } + + Ok(AdjacencyIndex { + vertex_to_edges, + vertex_to_simplices, + simplex_to_neighbors, + }) + } + + #[must_use] + fn collect_edges(&self) -> FastHashSet { + let simplex_cap = self.tds.number_of_simplices(); + let edges_per_simplex = (D + 1).saturating_mul(D) / 2; + + let mut edges: FastHashSet = + fast_hash_set_with_capacity(simplex_cap.saturating_mul(edges_per_simplex)); + + for (_simplex_key, simplex) in self.tds.simplices() { + let vertices = simplex.vertices(); + for i in 0..vertices.len() { + for j in (i + 1)..vertices.len() { + edges.insert(EdgeKey::new(vertices[i], vertices[j])); + } + } + } + + edges + } + + #[must_use] + fn collect_incident_edges(&self, v: VertexKey) -> FastHashSet { + let mut edges: FastHashSet = FastHashSet::default(); + + for simplex_key in self.adjacent_simplices(v) { + let Some(simplex) = self.tds.simplex(simplex_key) else { + continue; + }; + + for &other in simplex.vertices() { + if other == v { + continue; + } + edges.insert(EdgeKey::new(v, other)); + } + } + + edges + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::geometry::kernel::FastKernel; + use crate::triangulation::DelaunayTriangulation; + use crate::vertex; + + use slotmap::KeyData; + use std::collections::HashSet; + + /// Basic accessor tests across dimensions. + macro_rules! test_basic_accessors { + ($dim:expr, [$($simplex_coords:expr),+ $(,)?]) => { + pastey::paste! { + #[test] + fn []() { + let empty: Triangulation, (), (), $dim> = + Triangulation::new_empty(FastKernel::new()); + assert_eq!(empty.number_of_vertices(), 0); + assert_eq!(empty.number_of_simplices(), 0); + assert_eq!(empty.dim(), -1); + assert_eq!(empty.simplices().count(), 0); + assert_eq!(empty.vertices().count(), 0); + assert_eq!(empty.facets().count(), 0); + assert_eq!(empty.boundary_facets().count(), 0); + + let vertices = vec![ + $(vertex!($simplex_coords)),+ + ]; + let expected_vertex_count = vertices.len(); + + let tds = + Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) + .unwrap(); + let tri = Triangulation::, (), (), $dim>::new_with_tds( + FastKernel::new(), + tds, + ); + + assert_eq!(tri.number_of_vertices(), expected_vertex_count); + assert_eq!(tri.number_of_simplices(), 1); + assert_eq!(tri.dim(), $dim as i32); + assert_eq!(tri.simplices().count(), 1); + assert_eq!(tri.vertices().count(), expected_vertex_count); + assert_eq!(tri.facets().count(), expected_vertex_count); + assert_eq!(tri.boundary_facets().count(), expected_vertex_count); + } + } + }; + } + + test_basic_accessors!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); + test_basic_accessors!( + 3, + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0] + ] + ); + test_basic_accessors!( + 4, + [ + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0] + ] + ); + test_basic_accessors!( + 5, + [ + [0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0] + ] + ); + + #[test] + fn topology_edges_triangle_2d() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + + let dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + + assert_eq!(tri.number_of_simplices(), 1); + assert_eq!(tri.number_of_vertices(), 3); + assert_eq!(tri.number_of_edges(), 3); + + let edges: HashSet<_> = tri.edges().collect(); + assert_eq!(edges.len(), 3); + + let index = tri.build_adjacency_index().unwrap(); + let edges_with_index: HashSet<_> = tri.edges_with_index(&index).collect(); + assert_eq!(edges_with_index, edges); + assert_eq!(tri.number_of_edges_with_index(&index), 3); + + assert!(edges.iter().all(|edge| { + let (a, b) = edge.endpoints(); + a != b && tri.vertex_coords(a).is_some() && tri.vertex_coords(b).is_some() + })); + } + + #[test] + fn topology_edges_and_incident_edges_double_tetrahedron_3d() { + let vertices = vec![ + vertex!([0.0, 0.0, 0.0]), + vertex!([2.0, 0.0, 0.0]), + vertex!([1.0, 2.0, 0.0]), + vertex!([1.0, 0.7, 1.5]), + vertex!([1.0, 0.7, -1.5]), + ]; + + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + + assert_eq!(tri.number_of_simplices(), 2); + assert_eq!(tri.number_of_vertices(), 5); + assert_eq!(tri.number_of_edges(), 9); + + let base_vertex_key = tri + .vertices() + .find_map(|(vk, _)| (tri.vertex_coords(vk)? == [0.0, 0.0, 0.0]).then_some(vk)) + .unwrap(); + assert_eq!(tri.number_of_incident_edges(base_vertex_key), 4); + + let index = tri.build_adjacency_index().unwrap(); + assert_eq!(tri.number_of_edges_with_index(&index), 9); + assert_eq!(tri.adjacent_simplices(base_vertex_key).count(), 2); + assert_eq!( + tri.adjacent_simplices_with_index(&index, base_vertex_key) + .count(), + 2 + ); + assert_eq!( + tri.number_of_adjacent_simplices_with_index(&index, base_vertex_key), + 2 + ); + assert_eq!( + tri.incident_edges_with_index(&index, base_vertex_key) + .count(), + 4 + ); + assert_eq!( + tri.number_of_incident_edges_with_index(&index, base_vertex_key), + 4 + ); + + let apex_vertex_key = tri + .vertices() + .find_map(|(vk, _)| (tri.vertex_coords(vk)? == [1.0, 0.7, 1.5]).then_some(vk)) + .unwrap(); + assert_eq!(tri.number_of_incident_edges(apex_vertex_key), 3); + assert_eq!( + tri.adjacent_simplices_with_index(&index, apex_vertex_key) + .count(), + 1 + ); + assert_eq!( + tri.number_of_adjacent_simplices_with_index(&index, apex_vertex_key), + 1 + ); + + for (simplex_key, _) in tri.simplices() { + assert_eq!( + tri.simplex_neighbors_with_index(&index, simplex_key) + .count(), + 1 + ); + assert_eq!( + tri.number_of_simplex_neighbors_with_index(&index, simplex_key), + 1 + ); + } + } + + #[test] + fn topology_queries_missing_keys_are_empty_or_none() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + let index = tri.build_adjacency_index().unwrap(); + + let missing_vertex_key = VertexKey::default(); + assert_eq!(tri.adjacent_simplices(missing_vertex_key).count(), 0); + assert_eq!( + tri.adjacent_simplices_with_index(&index, missing_vertex_key) + .count(), + 0 + ); + assert_eq!( + tri.number_of_adjacent_simplices_with_index(&index, missing_vertex_key), + 0 + ); + assert_eq!(tri.incident_edges(missing_vertex_key).count(), 0); + assert_eq!( + tri.incident_edges_with_index(&index, missing_vertex_key) + .count(), + 0 + ); + assert_eq!(tri.number_of_incident_edges(missing_vertex_key), 0); + assert_eq!( + tri.number_of_incident_edges_with_index(&index, missing_vertex_key), + 0 + ); + assert!(tri.vertex_coords(missing_vertex_key).is_none()); + + let missing_simplex_key = SimplexKey::default(); + assert_eq!(tri.simplex_neighbors(missing_simplex_key).count(), 0); + assert_eq!( + tri.simplex_neighbors_with_index(&index, missing_simplex_key) + .count(), + 0 + ); + assert_eq!( + tri.number_of_simplex_neighbors_with_index(&index, missing_simplex_key), + 0 + ); + assert!(tri.simplex_vertices(missing_simplex_key).is_none()); + } + + #[test] + fn topology_geometry_accessors_round_trip() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + + let dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + + let vertex_key = tri + .vertices() + .find_map(|(vk, _)| (tri.vertex_coords(vk)? == [1.0, 0.0]).then_some(vk)) + .unwrap(); + assert_eq!(tri.vertex_coords(vertex_key).unwrap(), [1.0, 0.0]); + + let simplex_key = tri.simplices().next().unwrap().0; + let simplex_vertices = tri.simplex_vertices(simplex_key).unwrap(); + assert_eq!(simplex_vertices.len(), 3); + assert!(simplex_vertices.contains(&vertex_key)); + } + + #[test] + fn build_adjacency_index_basic_invariants() { + let vertices = vec![ + vertex!([0.0, 0.0, 0.0]), + vertex!([2.0, 0.0, 0.0]), + vertex!([1.0, 2.0, 0.0]), + vertex!([1.0, 0.7, 1.5]), + vertex!([1.0, 0.7, -1.5]), + ]; + + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + let index = tri.build_adjacency_index().unwrap(); + + let simplex_keys: Vec<_> = tri.simplices().map(|(ck, _)| ck).collect(); + assert_eq!(simplex_keys.len(), 2); + for &simplex_key in &simplex_keys { + let neighbors = index.simplex_to_neighbors.get(&simplex_key).unwrap(); + assert_eq!(neighbors.len(), 1); + assert!(simplex_keys.contains(&neighbors[0])); + assert_ne!(neighbors[0], simplex_key); + } + + for (vertex_key, _) in tri.vertices() { + let simplices = index.vertex_to_simplices.get(&vertex_key).unwrap(); + assert!(!simplices.is_empty()); + + let edges = index.vertex_to_edges.get(&vertex_key).unwrap(); + assert!(!edges.is_empty()); + assert!(edges.iter().all( + |edge| matches!(edge.endpoints(), (a, b) if a == vertex_key || b == vertex_key) + )); + } + } + + #[test] + fn build_adjacency_index_empty_triangulation_is_empty() { + let tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + let index = tri.build_adjacency_index().unwrap(); + assert!(index.vertex_to_simplices.is_empty()); + assert!(index.simplex_to_neighbors.is_empty()); + assert!(index.vertex_to_edges.is_empty()); + } + + #[test] + fn build_adjacency_index_includes_isolated_vertex_entries() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + let isolated_vertex = tri + .tds + .insert_vertex_with_mapping(vertex!([10.0, 10.0])) + .unwrap(); + let index = tri.build_adjacency_index().unwrap(); + + assert!( + index + .vertex_to_simplices + .get(&isolated_vertex) + .is_some_and(SmallBuffer::is_empty) + ); + assert!( + index + .vertex_to_edges + .get(&isolated_vertex) + .is_some_and(SmallBuffer::is_empty) + ); + } + + #[test] + fn build_adjacency_index_errors_on_missing_neighbor_simplex() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let simplex_key = tri.tds.simplex_keys().next().unwrap(); + + let mut missing_neighbor = SimplexKey::default(); + if tri.tds.contains_simplex(missing_neighbor) { + missing_neighbor = SimplexKey::from(KeyData::from_ffi(u64::MAX)); + } + assert!(!tri.tds.contains_simplex(missing_neighbor)); + + tri.tds + .simplex_mut(simplex_key) + .unwrap() + .set_neighbors_from_keys([Some(missing_neighbor), None, None]) + .unwrap(); + + match tri.build_adjacency_index() { + Err(AdjacencyIndexBuildError::MissingNeighborSimplex { + simplex_key: err_simplex_key, + neighbor_key, + }) => { + assert_eq!(err_simplex_key, simplex_key); + assert_eq!(neighbor_key, missing_neighbor); + } + other => panic!("Expected MissingNeighborSimplex, got {other:?}"), + } + } + + #[test] + fn simplex_neighbors_filters_missing_neighbor_simplex() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let simplex_key = tri.tds.simplex_keys().next().unwrap(); + let missing_neighbor = SimplexKey::from(KeyData::from_ffi(u64::MAX)); + assert!(!tri.tds.contains_simplex(missing_neighbor)); + + tri.tds + .simplex_mut(simplex_key) + .unwrap() + .set_neighbors_from_keys([Some(missing_neighbor), None, None]) + .unwrap(); + + assert_eq!(tri.simplex_neighbors(simplex_key).count(), 0); + } + + #[test] + fn build_adjacency_index_errors_on_missing_vertex_key() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let simplex_key = tri.tds.simplex_keys().next().unwrap(); + let existing_vertices = tri.tds.simplex(simplex_key).unwrap().vertices().to_vec(); + + let mut missing_vertex = VertexKey::default(); + if tri.tds.contains_vertex_key(missing_vertex) { + missing_vertex = VertexKey::from(KeyData::from_ffi(u64::MAX)); + } + assert!(!tri.tds.contains_vertex_key(missing_vertex)); + + { + let simplex = tri.tds.simplex_mut(simplex_key).unwrap(); + simplex.clear_vertex_keys(); + simplex.push_vertex_key(existing_vertices[0]); + simplex.push_vertex_key(existing_vertices[1]); + simplex.push_vertex_key(missing_vertex); + } + + match tri.build_adjacency_index() { + Err(AdjacencyIndexBuildError::MissingVertexKey { + simplex_key: err_simplex_key, + vertex_key, + }) => { + assert_eq!(err_simplex_key, simplex_key); + assert_eq!(vertex_key, missing_vertex); + } + other => panic!("Expected MissingVertexKey, got {other:?}"), + } + } + + #[test] + fn topology_queries_on_two_tet_triangulation() { + let vertices = [ + 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]), + vertex!([1.0, 1.0, 1.0]), + ]; + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + + let edge_count = tri.number_of_edges(); + let edges_collected: HashSet<_> = tri.edges().collect(); + assert_eq!(edges_collected.len(), edge_count); + assert!(edge_count >= 6); + + assert!(tri.facets().next().is_some()); + assert!(tri.boundary_facets().next().is_some()); + + let (simplex_key, _) = tri.simplices().next().unwrap(); + let simplex_vertices = tri.simplex_vertices(simplex_key).unwrap(); + assert_eq!(simplex_vertices.len(), 4); + for &vertex_key in simplex_vertices { + let coords = tri.vertex_coords(vertex_key).unwrap(); + assert_eq!(coords.len(), 3); + } + + assert!( + tri.simplex_vertices(SimplexKey::from(KeyData::from_ffi(0xDEAD))) + .is_none() + ); + assert!( + tri.vertex_coords(VertexKey::from(KeyData::from_ffi(0xBEEF))) + .is_none() + ); + + let vertex_key = tri.vertices().next().unwrap().0; + assert!(tri.adjacent_simplices(vertex_key).next().is_some()); + assert!(tri.simplex_neighbors(simplex_key).next().is_some()); + + let incident_edges: Vec<_> = tri.incident_edges(vertex_key).collect(); + assert!(!incident_edges.is_empty()); + assert_eq!( + tri.number_of_incident_edges(vertex_key), + incident_edges.len() + ); + } + + #[test] + fn adjacency_index_with_index_methods() { + let vertices = [ + 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]), + vertex!([1.0, 1.0, 1.0]), + ]; + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + let index = tri.build_adjacency_index().unwrap(); + + let idx_edges: HashSet<_> = tri.edges_with_index(&index).collect(); + let direct_edges: HashSet<_> = tri.edges().collect(); + assert_eq!(idx_edges, direct_edges); + assert_eq!( + tri.number_of_edges_with_index(&index), + tri.number_of_edges() + ); + + let vertex_key = tri.vertices().next().unwrap().0; + let idx_adj: HashSet<_> = tri + .adjacent_simplices_with_index(&index, vertex_key) + .collect(); + let direct_adj: HashSet<_> = tri.adjacent_simplices(vertex_key).collect(); + assert_eq!(idx_adj, direct_adj); + assert_eq!( + tri.number_of_adjacent_simplices_with_index(&index, vertex_key), + direct_adj.len() + ); + + let simplex_key = tri.simplices().next().unwrap().0; + let direct_neighbors: Vec<_> = tri.simplex_neighbors(simplex_key).collect(); + assert_eq!( + tri.simplex_neighbors_with_index(&index, simplex_key) + .count(), + direct_neighbors.len() + ); + assert_eq!( + tri.number_of_simplex_neighbors_with_index(&index, simplex_key), + direct_neighbors.len() + ); + + let idx_incident: HashSet<_> = tri.incident_edges_with_index(&index, vertex_key).collect(); + let direct_incident: HashSet<_> = tri.incident_edges(vertex_key).collect(); + assert_eq!(idx_incident, direct_incident); + assert_eq!( + tri.number_of_incident_edges_with_index(&index, vertex_key), + direct_incident.len() + ); + } +} diff --git a/src/core/repair.rs b/src/core/repair.rs new file mode 100644 index 00000000..e2a01c0a --- /dev/null +++ b/src/core/repair.rs @@ -0,0 +1,1619 @@ +//! Local topology repair for generic triangulations. +//! +//! This module owns local facet issue detection/repair, stale incident-simplex +//! repair, and vertex-removal cavity retriangulation for [`Triangulation`](crate::core::triangulation::Triangulation). + +use crate::core::algorithms::incremental_insertion::{ + CavityFillingError, InsertionError, external_facets_for_boundary, repair_neighbor_pointers, + repair_neighbor_pointers_local, wire_cavity_neighbors, +}; +use crate::core::algorithms::locate::extract_cavity_boundary; +use crate::core::collections::{ + FacetIssuesMap, FastHasher, MAX_PRACTICAL_DIMENSION_SIZE, SimplexKeyBuffer, SimplexKeySet, + SmallBuffer, fast_hash_map_with_capacity, fast_hash_set_with_capacity, +}; +use crate::core::facet::FacetHandle; +use crate::core::simplex::Simplex; +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}; +use crate::geometry::kernel::Kernel; +use crate::geometry::quality::{QualityError, QualitySimplexVerticesError, radius_ratio}; +use crate::geometry::util::safe_scalar_to_f64; +use core::ops::Div; +use num_traits::NumCast; +use std::env; +use std::hash::{Hash, Hasher}; +use std::sync::OnceLock; +use uuid::Uuid; + +static FORCE_GLOBAL_NEIGHBOR_REBUILD_ENABLED: OnceLock = OnceLock::new(); + +/// Returns whether local neighbor repair should be bypassed for regression isolation. +fn force_global_neighbor_rebuild_enabled() -> bool { + *FORCE_GLOBAL_NEIGHBOR_REBUILD_ENABLED + .get_or_init(|| env::var_os("DELAUNAY_FORCE_GLOBAL_NEIGHBOR_REBUILD").is_some()) +} + +/// Preserve typed TDS lookup/count failures from quality evaluation where possible. +fn quality_error_to_tds_error(simplex_key: SimplexKey, error: QualityError) -> TdsError { + match error { + QualityError::SimplexVertices { source, .. } => match source { + QualitySimplexVerticesError::SimplexNotFound { + simplex_key, + context, + } => TdsError::SimplexNotFound { + simplex_key, + context, + }, + QualitySimplexVerticesError::ReferencedVertexNotFound { + vertex_key, + context, + } => TdsError::VertexNotFound { + vertex_key, + context, + }, + QualitySimplexVerticesError::UnexpectedTdsFailure { message } => { + TdsError::InconsistentDataStructure { + message: format!( + "Quality evaluation failed for simplex {simplex_key:?}: {message}" + ), + } + } + }, + QualityError::VertexNotFound { vertex_key } => TdsError::VertexNotFound { + vertex_key, + context: format!("quality evaluation for simplex {simplex_key:?}"), + }, + QualityError::InvalidSimplexArity { + actual, + expected, + dimension, + } => TdsError::DimensionMismatch { + expected, + actual, + context: format!("quality evaluation for {dimension}D simplex {simplex_key:?}"), + }, + other => TdsError::InconsistentDataStructure { + message: format!("Quality evaluation failed for simplex {simplex_key:?}: {other}"), + }, + } +} + +/// Internal result from over-shared-facet repair, including the surviving frontier +/// that should seed local neighbor-pointer repair. +pub(crate) struct LocalFacetRepairOutcome { + /// Number of simplices actually removed from the TDS. + pub(crate) removed_count: usize, + /// Simplices selected for removal before they were deleted. + #[cfg_attr( + not(debug_assertions), + expect( + dead_code, + reason = "Removed-simplex keys are retained for debug logging and future local repair diagnostics" + ) + )] + pub(crate) removed_simplices: SimplexKeyBuffer, + /// Surviving one-hop neighbors whose back-references may have been cleared. + pub(crate) frontier_simplices: SimplexKeyBuffer, +} + +impl Triangulation +where + K: Kernel, + U: DataType, + V: DataType, +{ + /// Repair stale incident-simplex pointers and detect truly isolated vertices. + /// + /// After cavity filling and simplex removal, pre-existing boundary vertices may + /// still reference deleted conflict-region simplices via a stale `incident_simplex`. + /// For vertices with stale or missing `incident_simplex` values, this scans + /// simplices once until it has found a live incident simplex for every stale + /// vertex. Returns an error only if a vertex is in zero simplices (truly + /// isolated). + pub(crate) fn repair_stale_incident_simplices(&mut self) -> Result<(), InsertionError> { + let stale_vertices: Vec<_> = { + let tds = &self.tds; + tds.vertices() + .filter(|(vk, v)| { + !v.incident_simplex().is_some_and(|simplex_key| { + tds.simplex(simplex_key) + .is_some_and(|simplex| simplex.contains_vertex(*vk)) + }) + }) + .map(|(vk, v)| (vk, v.uuid())) + .collect() + }; + if stale_vertices.is_empty() { + return Ok(()); + } + + #[cfg(debug_assertions)] + if env::var_os("DELAUNAY_DEBUG_HULL").is_some() { + tracing::debug!( + stale_count = stale_vertices.len(), + "repairing stale incident-simplex pointers" + ); + } + + let mut stale_vertex_keys = fast_hash_set_with_capacity(stale_vertices.len()); + for &(vk, _) in &stale_vertices { + stale_vertex_keys.insert(vk); + } + let mut incident_simplex_by_vertex = fast_hash_map_with_capacity(stale_vertices.len()); + + 'simplices: for (simplex_key, simplex) in self.tds.simplices() { + for &vertex_key in simplex.vertices() { + if stale_vertex_keys.remove(&vertex_key) { + incident_simplex_by_vertex.insert(vertex_key, simplex_key); + if stale_vertex_keys.is_empty() { + break 'simplices; + } + } + } + } + + for &(vk, uuid) in &stale_vertices { + if let Some(&simplex_key) = incident_simplex_by_vertex.get(&vk) { + if let Some(vertex) = self.tds.vertex_mut(vk) { + vertex.set_incident_simplex(Some(simplex_key)); + } + } else { + // Truly isolated: no simplex in the TDS contains this vertex. + return Err(InsertionError::TopologyValidationFailed { + message: "Truly isolated vertex detected during stale incident-simplex repair" + .to_string(), + source: TriangulationValidationError::IsolatedVertex { + vertex_key: vk, + vertex_uuid: uuid, + }, + }); + } + } + Ok(()) + } + + /// Repair neighbor pointers after local simplex removal without scanning the full TDS. + pub(crate) fn repair_neighbors_after_local_simplex_removal( + &mut self, + new_simplices: &SimplexKeyBuffer, + frontier_simplices: &[SimplexKey], + ) -> Result { + #[cfg(debug_assertions)] + tracing::debug!( + simplices = self.tds.number_of_simplices(), + surviving_new_simplex_seeds = new_simplices + .iter() + .filter(|&&simplex_key| self.tds.contains_simplex(simplex_key)) + .count(), + frontier_simplex_seeds = frontier_simplices + .iter() + .filter(|&&simplex_key| self.tds.contains_simplex(simplex_key)) + .count(), + "Before local neighbor-pointer repair" + ); + + if force_global_neighbor_rebuild_enabled() { + #[cfg(debug_assertions)] + tracing::debug!( + "DELAUNAY_FORCE_GLOBAL_NEIGHBOR_REBUILD set; using global neighbor rebuild" + ); + return repair_neighbor_pointers(&mut self.tds).map_err(|source| { + CavityFillingError::NeighborRebuild { + reason: source.into(), + } + .into() + }); + } + + #[cfg(debug_assertions)] + { + match repair_neighbor_pointers_local( + &mut self.tds, + new_simplices, + Some(frontier_simplices), + ) { + Ok(repaired) => Ok(repaired), + Err(local_error) => { + tracing::warn!( + error = %local_error, + "Local neighbor-pointer repair failed; falling back to global rebuild in debug mode" + ); + repair_neighbor_pointers(&mut self.tds).map_err(|source| { + CavityFillingError::NeighborRebuild { + reason: source.into(), + } + .into() + }) + } + } + } + + #[cfg(not(debug_assertions))] + { + repair_neighbor_pointers_local(&mut self.tds, new_simplices, Some(frontier_simplices)) + .map_err(|source| { + CavityFillingError::NeighborRebuild { + reason: source.into(), + } + .into() + }) + } + } +} + +impl Triangulation +where + K: Kernel, + K::Scalar: NumCast, + U: DataType, + V: DataType, +{ + /// Removes a vertex and retriangulates the resulting cavity using fan triangulation. + /// + /// This operation maintains topological consistency by: + /// 1. Finding all simplices containing the vertex + /// 2. Removing those simplices (creating a cavity) + /// 3. Extracting the cavity boundary facets + /// 4. Filling the cavity with a fan triangulation (pick apex, connect to all boundary facets) + /// 5. Wiring neighbors to maintain consistency + /// 6. Removing the vertex itself + /// + /// **Fan Triangulation**: The cavity is filled by picking one boundary vertex as an apex + /// and connecting it to all boundary facets. This is fast and maintains all topological + /// invariants, though it may create poorly-shaped simplices in some cases. + /// + /// # Arguments + /// + /// * `vertex_key` - Key of the vertex to remove + /// + /// # Returns + /// + /// The number of simplices that were removed along with the vertex. + /// + /// # Errors + /// + /// Returns [`InvariantError`] if the removal cannot be completed while maintaining + /// triangulation invariants. The error preserves structured information from whichever + /// layer (TDS or Topology) detected the failure. + pub(crate) fn remove_vertex(&mut self, vertex_key: VertexKey) -> Result { + // Verify the vertex exists + if self.tds.vertex(vertex_key).is_none() { + return Ok(0); // Vertex not found, nothing to remove + } + + // Collect all simplices containing this vertex by scanning all simplices + let simplices_to_remove: SimplexKeyBuffer = self + .tds + .simplices() + .filter_map(|(simplex_key, simplex)| { + if simplex.vertices().contains(&vertex_key) { + Some(simplex_key) + } else { + None + } + }) + .collect(); + + if simplices_to_remove.is_empty() { + // Vertex exists but has no incident simplices - use Tds removal + return self + .tds + .remove_vertex(vertex_key) + .map_err(|e| InvariantError::Tds(e.into_inner())); + } + + // Extract cavity boundary BEFORE removing simplices + let boundary_facets = + extract_cavity_boundary(&self.tds, &simplices_to_remove).map_err(|e| { + TdsError::InconsistentDataStructure { + message: format!("Failed to extract cavity boundary: {e}"), + } + })?; + + // If boundary is empty, we're removing the entire triangulation + if boundary_facets.is_empty() { + // Use Tds removal for empty boundary case + return self + .tds + .remove_vertex(vertex_key) + .map_err(|e| InvariantError::Tds(e.into_inner())); + } + + // Pick apex vertex for fan triangulation (first vertex of first boundary facet) + let apex_vertex_key = self.pick_fan_apex(&boundary_facets)?; + + // Snapshot before destructive retriangulation edits so we can roll back if any + // subsequent orientation/finalization step fails. + let tds_snapshot = self.tds.clone_for_rollback(); + let retriangulation_result = (|| -> Result { + // Fill cavity with fan triangulation BEFORE removing old simplices + // Use fan triangulation that skips boundary facets which already include the apex + let new_simplices = self + .fan_fill_cavity(apex_vertex_key, &boundary_facets) + .map_err(|e| insertion_error_to_invariant_error(e, "Fan triangulation failed"))?; + // Wire neighbors for the new simplices (while both old and new simplices exist) + let external_facets = + external_facets_for_boundary(&self.tds, &simplices_to_remove, &boundary_facets) + .map_err(|e| { + insertion_error_to_invariant_error(e, "External-facet collection failed") + })?; + wire_cavity_neighbors( + &mut self.tds, + &new_simplices, + external_facets.iter().copied(), + Some(&simplices_to_remove), + ) + .map_err(|e| insertion_error_to_invariant_error(e, "Neighbor wiring failed"))?; + + // Remove the simplices containing the vertex (now that new simplices are wired up) + // Note: remove_simplices_by_keys() automatically clears neighbor pointers in surviving + // simplices that reference removed simplices (sets them to None/boundary) + let mut simplices_removed = self.tds.remove_simplices_by_keys(&simplices_to_remove); + + // Validate facet topology for newly created simplices (O(k*D) localized check) + if let Some(issues) = self.detect_local_facet_issues(&new_simplices)? { + #[cfg(debug_assertions)] + tracing::warn!( + "Warning: {} over-shared facets detected after vertex removal, repairing...", + issues.len() + ); + let repair_outcome = self + .repair_local_facet_issues_with_frontier(&issues) + .map_err(InvariantError::Tds)?; + let removed = repair_outcome.removed_count; + simplices_removed += removed; + #[cfg(debug_assertions)] + tracing::debug!("Repaired by removing {removed} additional simplices"); + + // Repair neighbor pointers after removing additional simplices + // This ensures neighbor consistency after repair operations + if removed > 0 { + repair_neighbor_pointers(&mut self.tds).map_err(|e| { + insertion_error_to_invariant_error( + e, + "Neighbor repair after facet issue repair failed", + ) + })?; + } + } + // Normalize coherent orientation, canonicalize global sign, and promote + // simplices to positive orientation (#258). + self.normalize_and_promote_positive_orientation() + .map_err(|e| { + insertion_error_to_invariant_error( + e, + "Orientation canonicalization failed after fan retriangulation", + ) + })?; + + // Rebuild vertex-simplex incidence for all vertices + self.tds + .assign_incident_simplices() + .map_err(|e| InvariantError::Tds(e.into_inner()))?; + + // Remove the vertex using Tds method (handles internal bookkeeping) + self.tds + .remove_vertex(vertex_key) + .map_err(|e| InvariantError::Tds(e.into_inner()))?; + + Ok(simplices_removed) + })(); + + match retriangulation_result { + Ok(simplices_removed) => Ok(simplices_removed), + Err(error) => { + self.tds = tds_snapshot; + Err(error) + } + } + } + + /// Pick an apex vertex for fan triangulation. + /// + /// Selects the first vertex from the first boundary facet as the apex. + /// The fan will connect this apex to all boundary facets. + /// + /// # Arguments + /// + /// * `boundary_facets` - The cavity boundary facets + /// + /// # Returns + /// + /// The vertex key to use as apex. + /// + /// # Errors + /// + /// Returns a typed [`TdsError`] if the boundary is empty, references a missing + /// simplex, or carries an out-of-range facet index. + fn pick_fan_apex(&self, boundary_facets: &[FacetHandle]) -> Result { + // Get first boundary facet + let first_facet = boundary_facets + .first() + .ok_or_else(|| TdsError::DimensionMismatch { + expected: 1, + actual: 0, + context: "fan apex selection requires at least one cavity-boundary facet" + .to_string(), + })?; + let simplex = self.tds.simplex(first_facet.simplex_key()).ok_or_else(|| { + TdsError::SimplexNotFound { + simplex_key: first_facet.simplex_key(), + context: "fan apex selection".to_string(), + } + })?; + + // Get the first vertex from this facet (any vertex that's not the opposite one) + let facet_idx = >::from(first_facet.facet_index()); + if facet_idx >= simplex.number_of_vertices() { + return Err(TdsError::IndexOutOfBounds { + index: facet_idx, + bound: simplex.number_of_vertices(), + context: format!( + "fan apex selection for boundary simplex {:?}", + first_facet.simplex_key() + ), + }); + } + simplex + .vertices() + .iter() + .enumerate() + .find(|(i, _)| *i != facet_idx) + .map(|(_, &vkey)| vkey) + .ok_or_else(|| TdsError::DimensionMismatch { + expected: 2, + actual: simplex.number_of_vertices(), + context: format!( + "fan apex selection for boundary simplex {:?}", + first_facet.simplex_key() + ), + }) + } + + /// Fan-specific cavity fill: connect an existing apex vertex to boundary facets + /// that do not already include the apex. This avoids creating degenerate simplices + /// with duplicate vertices when the apex lies on a boundary facet. + fn fan_fill_cavity( + &mut self, + apex_vertex_key: VertexKey, + boundary_facets: &[FacetHandle], + ) -> Result { + let mut new_simplices = SimplexKeyBuffer::new(); + + for facet_handle in boundary_facets { + let boundary_simplex = + self.tds + .simplex(facet_handle.simplex_key()) + .ok_or_else(|| CavityFillingError::MissingBoundarySimplex { + simplex_key: facet_handle.simplex_key(), + })?; + + let facet_idx = >::from(facet_handle.facet_index()); + if facet_idx >= boundary_simplex.number_of_vertices() { + return Err(CavityFillingError::InvalidFacetIndex { + simplex_key: facet_handle.simplex_key(), + facet_index: facet_idx, + vertex_count: boundary_simplex.number_of_vertices(), + } + .into()); + } + + // Gather facet vertices (all except the opposite vertex) + let mut facet_vertices = SmallBuffer::::new(); + for (i, &vkey) in boundary_simplex.vertices().iter().enumerate() { + if i != facet_idx { + facet_vertices.push(vkey); + } + } + + // Skip facets that already contain the apex to avoid duplicate vertices + if facet_vertices.contains(&apex_vertex_key) { + continue; + } + + // Build new simplex vertices = facet_vertices + apex + let mut new_simplex_vertices = facet_vertices; + new_simplex_vertices.push(apex_vertex_key); + + // Create and insert the new simplex + let new_simplex = + Simplex::new(new_simplex_vertices, None).map_err(CavityFillingError::from)?; + let simplex_key = self + .tds + .insert_simplex_with_mapping_prechecked_topology(new_simplex) + .map_err(InsertionError::from)?; + + new_simplices.push(simplex_key); + } + + if new_simplices.is_empty() { + return Err(CavityFillingError::EmptyFanTriangulation.into()); + } + + Ok(new_simplices) + } + + /// Detects over-shared facets + /// + /// This is an **O(k * D)** operation where k = number of simplices to check, + /// unlike global validation which is O(N * D) for the entire triangulation. + /// + /// # Performance + /// + /// - **Complexity**: O(k * D) where k = `simplices.len()`, D = dimension + /// - **Use case**: Detect issues in newly created simplices after insertion/removal + /// - **Comparison**: Global detection is O(N * D) where N = total simplices + /// + /// # Arguments + /// + /// * `simplices` - Keys of simplices to check (typically newly created simplices) + /// + /// # Returns + /// + /// `Ok(None)` if all facets are valid (≤2 simplices per facet). + /// `Ok(Some(issues))` if over-shared facets are detected, where issues is a map + /// from facet hash to the simplices sharing that facet. + /// + /// # Errors + /// + /// Returns error if simplices cannot be accessed. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::DelaunayTriangulation; + /// use delaunay::prelude::triangulation::vertex; + /// + /// // A single simplex has no over-shared facets. + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// let simplex_keys: Vec<_> = dt.simplices().map(|(ck, _)| ck).collect(); + /// let issues = dt + /// .as_triangulation() + /// .detect_local_facet_issues(&simplex_keys) + /// .unwrap(); + /// assert!(issues.is_none()); + /// + /// // Note: This method is most useful for checking newly created simplices + /// // after insertion/removal operations (see usage in insert_transactional). + /// ``` + pub fn detect_local_facet_issues( + &self, + simplices: &[SimplexKey], + ) -> Result, TdsError> { + // Build facet map for ONLY the specified simplices + // This is O(k * D) instead of O(N * D) + let mut facet_to_simplices = FacetIssuesMap::default(); + + // Index facets from the specified simplices + for &simplex_key in simplices { + let Some(simplex) = self.tds.simplex(simplex_key) else { + continue; // Simplex was removed, skip + }; + + // For each facet of this simplex + for facet_idx in 0..simplex.number_of_vertices() { + // Compute facet hash from sorted vertex keys + let mut facet_vkeys = SmallBuffer::::new(); + for (i, &vkey) in simplex.vertices().iter().enumerate() { + if i != facet_idx { + facet_vkeys.push(vkey); + } + } + facet_vkeys.sort_unstable(); + + // Hash the facet + let mut hasher = FastHasher::default(); + for &vkey in &facet_vkeys { + vkey.hash(&mut hasher); + } + let facet_hash = hasher.finish(); + + // Track this simplex/facet pair + let facet_idx_u8 = + u8::try_from(facet_idx).map_err(|_| TdsError::IndexOutOfBounds { + index: facet_idx, + bound: u8::MAX as usize + 1, + context: "facet index exceeds u8 range (dimension too high)".to_string(), + })?; + facet_to_simplices + .entry(facet_hash) + .or_insert_with(SmallBuffer::new) + .push((simplex_key, facet_idx_u8)); + } + } + + // Filter to only over-shared facets (> 2 simplices) in a single pass + facet_to_simplices.retain(|_, simplex_facet_pairs| simplex_facet_pairs.len() > 2); + + if facet_to_simplices.is_empty() { + Ok(None) + } else { + Ok(Some(facet_to_simplices)) + } + } + + /// Select simplices to remove for over-shared-facet repair without mutating the TDS. + fn simplices_for_local_facet_issue_repair( + &self, + issues: &FacetIssuesMap, + ) -> Result + where + K::Scalar: Div, + { + let mut simplices_to_remove = SimplexKeySet::default(); + + // For each over-shared facet, select simplices to remove + for simplex_facet_pairs in issues.values() { + // Compute quality for each simplex - propagate errors from quality evaluation + let mut simplex_qualities: Vec<(SimplexKey, f64, Uuid)> = Vec::new(); + for &(simplex_key, _) in simplex_facet_pairs { + let simplex = + self.tds + .simplex(simplex_key) + .ok_or_else(|| TdsError::SimplexNotFound { + simplex_key, + context: "facet repair quality evaluation".to_string(), + })?; + let uuid = simplex.uuid(); + + // Propagate quality evaluation errors + let ratio = radius_ratio(self, simplex_key) + .map_err(|error| quality_error_to_tds_error(simplex_key, error))?; + let ratio_f64 = + safe_scalar_to_f64(ratio).map_err(|_| TdsError::InconsistentDataStructure { + message: format!( + "Quality ratio conversion failed for simplex {simplex_key:?}" + ), + })?; + + if ratio_f64.is_finite() { + simplex_qualities.push((simplex_key, ratio_f64, uuid)); + } else { + return Err(TdsError::InconsistentDataStructure { + message: format!( + "Non-finite quality ratio {ratio_f64} for simplex {simplex_key:?}" + ), + }); + } + } + + // Quality-based selection: keep 2 best, remove rest + // Note: simplex_qualities always has all involved_simplices at this point since + // any quality computation failure results in an early error return above + simplex_qualities + .sort_unstable_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.2.cmp(&b.2))); + + // Mark simplices beyond the top 2 for removal + for (simplex_key, _, _) in simplex_qualities.iter().skip(2) { + if self.tds.contains_simplex(*simplex_key) { + simplices_to_remove.insert(*simplex_key); + } + } + } + + Ok(simplices_to_remove.into_iter().collect()) + } + + /// Collect surviving simplices that need local neighbor repair after simplex removal. + fn collect_local_repair_frontier( + &self, + issues: &FacetIssuesMap, + simplices_to_remove: &[SimplexKey], + ) -> SimplexKeyBuffer { + if simplices_to_remove.is_empty() { + return SimplexKeyBuffer::new(); + } + + let removal_set: SimplexKeySet = simplices_to_remove.iter().copied().collect(); + let mut frontier = SimplexKeyBuffer::new(); + let mut issue_simplex_count = 0; + for pairs in issues.values() { + issue_simplex_count += pairs.len(); + } + let mut seen = + fast_hash_set_with_capacity(simplices_to_remove.len() * (D + 1) + issue_simplex_count); + + for &simplex_key in simplices_to_remove { + let Some(simplex) = self.tds.simplex(simplex_key) else { + continue; + }; + let Some(neighbors) = simplex.neighbor_keys() else { + continue; + }; + for neighbor_key in neighbors.flatten() { + if removal_set.contains(&neighbor_key) || !self.tds.contains_simplex(neighbor_key) { + continue; + } + if seen.insert(neighbor_key) { + frontier.push(neighbor_key); + } + } + } + + for simplex_facet_pairs in issues.values() { + for &(simplex_key, _) in simplex_facet_pairs { + if removal_set.contains(&simplex_key) || !self.tds.contains_simplex(simplex_key) { + continue; + } + if seen.insert(simplex_key) { + frontier.push(simplex_key); + } + } + } + + frontier + } + + /// Repair over-shared facets and return the local frontier for neighbor repair. + pub(crate) fn repair_local_facet_issues_with_frontier( + &mut self, + issues: &FacetIssuesMap, + ) -> Result + where + K::Scalar: Div, + { + let to_remove = self.simplices_for_local_facet_issue_repair(issues)?; + let frontier_simplices = self.collect_local_repair_frontier(issues, &to_remove); + let removed_count = self.tds.remove_simplices_by_keys(&to_remove); + + Ok(LocalFacetRepairOutcome { + removed_count, + removed_simplices: to_remove, + frontier_simplices, + }) + } + + /// Repairs over-shared facets by removing lower-quality simplices. + /// + /// Uses geometric quality metrics (`radius_ratio`) to select which simplices to keep + /// when a facet is shared by more than 2 simplices. UUID ordering is used as a tie-breaker + /// when simplices have equal quality. Errors if quality computation or conversion fails. + /// + /// # Performance + /// + /// - **Complexity**: O(m * q) where m = number of problematic facets, q = quality computation cost + /// - **Localized**: Only processes simplices involved in detected issues + /// + /// # Arguments + /// + /// * `issues` - Detected facet issues map from `detect_local_facet_issues()` + /// + /// # Returns + /// + /// Number of simplices removed during repair. + /// + /// This public wrapper is transactional: if removal, neighbor repair, + /// incident-simplex assignment, or final validation fails, the TDS is + /// restored to its pre-call state. + /// + /// # Errors + /// + /// Returns an [`InsertionError`] if quality evaluation, facet bookkeeping, + /// neighbor repair, incident-simplex assignment, or final topology + /// validation fails. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulation, DelaunayTriangulationConstructionError, + /// }; + /// use delaunay::prelude::triangulation::{FacetIssuesMap, InsertionError, vertex}; + /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] InsertionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { + /// // Start with a valid 2D simplex. + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices)?; + /// + /// // Empty issues map => nothing to remove. + /// let mut tri = dt.as_triangulation().clone(); + /// let removed = tri.repair_local_facet_issues(&FacetIssuesMap::default())?; + /// assert_eq!(removed, 0); + /// # Ok(()) + /// # } + /// ``` + /// + /// In practice, this method is typically called with issues detected by + /// [`detect_local_facet_issues`](Self::detect_local_facet_issues) after insertion/removal + /// operations. See `insert_transactional` for a typical usage pattern. + pub fn repair_local_facet_issues( + &mut self, + issues: &FacetIssuesMap, + ) -> Result + where + K::Scalar: Div, + { + let tds_snapshot = self.tds.clone_for_rollback(); + let repair_result = (|| -> Result { + let outcome = self + .repair_local_facet_issues_with_frontier(issues) + .map_err(InsertionError::TopologyValidation)?; + if outcome.removed_count == 0 { + return Ok(0); + } + + let new_simplices = SimplexKeyBuffer::new(); + self.repair_neighbors_after_local_simplex_removal( + &new_simplices, + &outcome.frontier_simplices, + )?; + self.tds + .assign_incident_simplices() + .map_err(|error| InsertionError::TopologyValidation(error.into_inner()))?; + self.validate() + .map_err(Self::invariant_error_to_insertion_error)?; + + Ok(outcome.removed_count) + })(); + + if repair_result.is_err() { + self.tds = tds_snapshot; + } + + repair_result + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::collections::{CavityBoundaryBuffer, NeighborBuffer}; + use crate::core::simplex::NeighborSlot; + use crate::core::tds::Tds; + use crate::core::vertex::Vertex; + use crate::geometry::kernel::FastKernel; + use crate::triangulation::DelaunayTriangulation; + use crate::vertex; + + use slotmap::KeyData; + + /// Helper: build a minimal 3D triangulation with one tetrahedron and valid + /// incident-simplex pointers for all four vertices. + fn build_single_tet() -> ( + Triangulation, (), (), 3>, + [VertexKey; 4], + SimplexKey, + ) { + let mut tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + let v0 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) + .unwrap(); + let v1 = tri + .tds + .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) + .unwrap(); + let v2 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) + .unwrap(); + let v3 = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) + .unwrap(); + + let ck = tri + .tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap()) + .unwrap(); + + for vk in [v0, v1, v2, v3] { + tri.tds + .vertex_mut(vk) + .unwrap() + .set_incident_simplex(Some(ck)); + } + + (tri, [v0, v1, v2, v3], ck) + } + + /// Build a deliberately invalid 2D fixture with three triangles sharing + /// one edge. The fixture is useful for local facet-repair tests because + /// removing one triangle leaves a small frontier whose survivor neighbor + /// slots need rewiring. + fn build_overshared_edge_fixture() -> ( + Triangulation, (), (), 2>, + [SimplexKey; 3], + VertexKey, + VertexKey, + ) { + let mut tds: Tds = Tds::empty(); + + let v_a = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let v_b = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); + let v_c = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); + let v_d = tds + .insert_vertex_with_mapping(vertex!([0.0, -1.0])) + .unwrap(); + let v_e = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap(); + + let c1 = tds + .insert_simplex_with_mapping(Simplex::new(vec![v_a, v_b, v_c], None).unwrap()) + .unwrap(); + let c2 = tds + .insert_simplex_with_mapping(Simplex::new(vec![v_a, v_b, v_d], None).unwrap()) + .unwrap(); + let c3 = tds + .insert_simplex_bypassing_topology_checks_for_test( + Simplex::new(vec![v_a, v_b, v_e], None).unwrap(), + ) + .unwrap(); + + for (simplex_key, neighbor_key) in [(c1, c2), (c2, c3), (c3, c1)] { + let simplex = tds.simplex_mut(simplex_key).unwrap(); + let mut neighbors = NeighborBuffer::>::new(); + neighbors.resize(3, None); + neighbors[2] = Some(neighbor_key); + simplex.set_neighbors_from_keys(neighbors).unwrap(); + } + tds.assign_incident_simplices().unwrap(); + + ( + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds), + [c1, c2, c3], + v_a, + v_b, + ) + } + + /// Consolidated macro for facet validation tests across dimensions. + /// + /// Verifies the manifold topology invariant: each facet shared by at most 2 simplices. + /// Consolidates detection and repair tests into comprehensive suites. + macro_rules! test_facet_validation { + ($dim:expr, [$($simplex_coords:expr),+ $(,)?]) => { + pastey::paste! { + #[test] + fn []() { + let vertices: Vec> = vec![ + $(vertex!($simplex_coords)),+ + ]; + + let tds = Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) + .unwrap(); + let tri = Triangulation::, (), (), $dim>::new_with_tds(FastKernel::new(), tds); + + // Valid simplex: should have no issues + let simplex_keys: Vec<_> = tri.tds.simplex_keys().collect(); + assert_eq!(simplex_keys.len(), 1); + let issues = tri.detect_local_facet_issues(&simplex_keys).unwrap(); + assert!(issues.is_none(), "{}D: Valid simplex should have no facet issues", $dim); + + // Empty list: should return None + let issues = tri.detect_local_facet_issues(&[]).unwrap(); + assert!(issues.is_none(), "{}D: Empty list should have no issues", $dim); + + // Nonexistent simplices: should be skipped gracefully + let fake_keys = vec![SimplexKey::default()]; + let issues = tri.detect_local_facet_issues(&fake_keys).unwrap(); + assert!(issues.is_none(), "{}D: Nonexistent simplices should be skipped", $dim); + + // Verify neighbors (all should be explicit boundary slots for a single simplex) + let (_, simplex) = tri.tds.simplices().next().unwrap(); + let neighbors = simplex + .neighbor_slots() + .expect("single simplex should assign boundary neighbor slots"); + assert!( + neighbors.iter().all(|slot| *slot == NeighborSlot::Boundary), + "{}D: Single simplex should have boundary slots", + $dim + ); + } + + #[test] + fn []() { + let vertices: Vec> = vec![ + $(vertex!($simplex_coords)),+ + ]; + + let tds = Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) + .unwrap(); + let mut tri = Triangulation::, (), (), $dim>::new_with_tds(FastKernel::new(), tds); + + // Empty issues map: should remove nothing + let empty_issues = FacetIssuesMap::default(); + let removed = tri.repair_local_facet_issues(&empty_issues).unwrap(); + assert_eq!(removed, 0, "{}D: Empty issues should remove 0 simplices", $dim); + assert_eq!(tri.tds.number_of_simplices(), 1, "{}D: Should still have 1 simplex", $dim); + } + } + }; + } + + /// Dimension-parametric `remove_vertex` tests. + /// + /// Verifies that vertex removal maintains neighbor pointer integrity and + /// triangulation validity across dimensions. + macro_rules! test_remove_vertex { + ($dim:expr, [$($simplex_coords:expr),+ $(,)?], $interior_point:expr) => { + pastey::paste! { + #[test] + fn []() { + // Build triangulation with D+1 simplex vertices + 1 interior point + let vertices: Vec> = { + let mut v = vec![$(vertex!($simplex_coords)),+]; + v.push(vertex!($interior_point)); + v + }; + + let mut dt = DelaunayTriangulation::new(&vertices) + .expect("Failed to create triangulation"); + + // Find and remove the interior vertex + let interior_vertex_key = dt + .vertices() + .find(|(_, v)| { + let coords = v.point().coords(); + coords.iter() + .zip($interior_point.iter()) + .all(|(a, b)| (a - b).abs() < 1e-10) + }) + .map(|(k, _)| k) + .expect("Interior vertex not found"); + + let initial_simplex_count = dt.tds().number_of_simplices(); + dt.remove_vertex(interior_vertex_key) + .expect("Failed to remove vertex"); + + // After removal, should have fewer simplices (or same if just 1 simplex left) + assert!(dt.tds().number_of_simplices() <= initial_simplex_count, + "{}D: Simplex count should not increase after removal", $dim); + + // Verify neighbor pointer consistency: + // 1. No dangling pointers (all neighbor keys exist) + // 2. Neighbor relationships are symmetric + for (simplex_key, simplex) in dt.tds().simplices() { + if let Some(neighbors) = simplex.neighbors() { + for (facet_idx, neighbor_opt) in neighbors.enumerate() { + if let Some(neighbor_key) = neighbor_opt { + // Verify neighbor exists + assert!( + dt.tds().contains_simplex(neighbor_key), + "{}D: Simplex {simplex_key:?} has neighbor pointer to non-existent simplex {neighbor_key:?}", + $dim + ); + + // Verify symmetry: neighbor should point back to us + let neighbor_simplex = dt + .tds() + .simplex(neighbor_key) + .expect("Neighbor simplex should exist"); + if let Some(mut neighbor_neighbors) = neighbor_simplex.neighbors() { + let points_back = neighbor_neighbors + .any(|neighbor| neighbor == Some(simplex_key)); + assert!( + points_back, + "{}D: Simplex {simplex_key:?} has neighbor {neighbor_key:?} at facet {facet_idx}, but neighbor doesn't point back", + $dim + ); + } + } + } + } + } + + // Verify triangulation is still valid (Levels 1–3; removal does not guarantee Delaunay) + let validation = dt.as_triangulation().validate(); + assert!( + validation.is_ok(), + "{}D: Triangulation should be structurally valid after vertex removal: {:?}", + $dim, + validation.err() + ); + } + } + }; + } + + // Facet validation tests (2D - 5D) + test_facet_validation!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); + test_facet_validation!( + 3, + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0] + ] + ); + test_facet_validation!( + 4, + [ + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0] + ] + ); + test_facet_validation!( + 5, + [ + [0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0] + ] + ); + + // Remove vertex tests (2D - 5D) + test_remove_vertex!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], [0.3, 0.3]); + test_remove_vertex!( + 3, + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0] + ], + [0.25, 0.25, 0.25] + ); + test_remove_vertex!( + 4, + [ + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0] + ], + [0.2, 0.2, 0.2, 0.2] + ); + test_remove_vertex!( + 5, + [ + [0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0] + ], + [0.16, 0.16, 0.16, 0.16, 0.16] + ); + + // ---- repair_stale_incident_simplices tests ---- + + #[test] + fn test_repair_stale_incident_simplices_noop_when_all_valid() { + let (mut tri, [v0, v1, v2, v3], ck) = build_single_tet(); + assert!(tri.repair_stale_incident_simplices().is_ok()); + + // Pointers unchanged. + for vk in [v0, v1, v2, v3] { + assert_eq!(tri.tds.vertex_mut(vk).unwrap().incident_simplex(), Some(ck)); + } + } + + #[test] + fn test_repair_stale_incident_simplices_repairs_none_pointer() { + let (mut tri, [_, _, _, v3], ck) = build_single_tet(); + + // Corrupt v3 to have no incident simplex. + tri.tds.vertex_mut(v3).unwrap().set_incident_simplex(None); + + assert!(tri.repair_stale_incident_simplices().is_ok()); + assert_eq!( + tri.tds.vertex_mut(v3).unwrap().incident_simplex(), + Some(ck), + "v3 should be repaired to point to the tetrahedron" + ); + } + + #[test] + fn test_repair_stale_incident_simplices_repairs_stale_pointer() { + let (mut tri, [_, _, _, v3], ck) = build_single_tet(); + + // Point v3 to a non-existent simplex key (simulates a deleted conflict simplex). + let stale = SimplexKey::from(KeyData::from_ffi(0xDEAD_BEEF)); + tri.tds + .vertex_mut(v3) + .unwrap() + .set_incident_simplex(Some(stale)); + + assert!(tri.repair_stale_incident_simplices().is_ok()); + assert_eq!( + tri.tds.vertex_mut(v3).unwrap().incident_simplex(), + Some(ck), + "stale pointer should be repaired to the valid simplex" + ); + } + + #[test] + fn test_repair_stale_incident_simplices_repairs_multiple_stale_pointers() { + let (mut tri, [v0, v1, v2, v3], ck) = build_single_tet(); + + let stale = SimplexKey::from(KeyData::from_ffi(0xDEAD_BEEF)); + tri.tds.vertex_mut(v0).unwrap().set_incident_simplex(None); + tri.tds + .vertex_mut(v2) + .unwrap() + .set_incident_simplex(Some(stale)); + + assert!(tri.repair_stale_incident_simplices().is_ok()); + for vk in [v0, v1, v2, v3] { + assert_eq!( + tri.tds.vertex(vk).unwrap().incident_simplex(), + Some(ck), + "all vertices should point to the live tetrahedron after one repair pass" + ); + } + } + + #[test] + fn test_repair_stale_incident_simplices_errors_on_truly_isolated_vertex() { + let (mut tri, _, _) = build_single_tet(); + + // Insert a vertex that is NOT referenced by any simplex. + let iso = tri + .tds + .insert_vertex_with_mapping(vertex!([0.5, 0.5, 0.5])) + .unwrap(); + + let result = tri.repair_stale_incident_simplices(); + assert!( + matches!( + &result, + Err(InsertionError::TopologyValidationFailed { + source, .. + }) if matches!( + source, + TriangulationValidationError::IsolatedVertex { vertex_key, .. } + if *vertex_key == iso + ) + ), + "Truly isolated vertex should produce IsolatedVertex error: {result:?}" + ); + } + + // ========================================================================= + // DETECT / REPAIR LOCAL FACET ISSUES + // ========================================================================= + + #[test] + fn test_detect_local_facet_issues_none_for_valid_triangulation() { + let vertices = [ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([1.0, 1.0]), + ]; + let dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + + let simplex_keys: Vec<_> = tri.simplices().map(|(ck, _)| ck).collect(); + let issues = tri.detect_local_facet_issues(&simplex_keys).unwrap(); + assert!(issues.is_none()); + } + + #[test] + fn test_pick_fan_apex_errors_for_empty_facets() { + let (tri, _, _) = build_single_tet(); + assert!(matches!( + tri.pick_fan_apex(&[]), + Err(TdsError::DimensionMismatch { + expected: 1, + actual: 0, + .. + }) + )); + } + + #[test] + fn test_pick_fan_apex_preserves_missing_boundary_simplex() { + let (tri, _, _) = build_single_tet(); + let missing_simplex = SimplexKey::from(KeyData::from_ffi(0xBAD)); + let facets = [FacetHandle::new(missing_simplex, 0)]; + + assert!(matches!( + tri.pick_fan_apex(&facets), + Err(TdsError::SimplexNotFound { + simplex_key, + .. + }) if simplex_key == missing_simplex + )); + } + + #[test] + fn test_quality_error_to_tds_error_preserves_lookup_variants() { + let simplex_key = SimplexKey::from(KeyData::from_ffi(1)); + let vertex_key = VertexKey::from(KeyData::from_ffi(2)); + + let simplex_error = QualityError::SimplexVertices { + simplex_key, + source: QualitySimplexVerticesError::SimplexNotFound { + simplex_key, + context: "quality lookup".to_string(), + }, + }; + assert!(matches!( + quality_error_to_tds_error(simplex_key, simplex_error), + TdsError::SimplexNotFound { simplex_key: key, .. } if key == simplex_key + )); + + let vertex_error = QualityError::VertexNotFound { vertex_key }; + assert!(matches!( + quality_error_to_tds_error(simplex_key, vertex_error), + TdsError::VertexNotFound { vertex_key: key, .. } if key == vertex_key + )); + } + + #[test] + fn test_quality_error_to_tds_error_preserves_arity_mismatch() { + let simplex_key = SimplexKey::from(KeyData::from_ffi(1)); + let error = QualityError::InvalidSimplexArity { + actual: 3, + expected: 4, + dimension: 3, + }; + + assert!(matches!( + quality_error_to_tds_error(simplex_key, error), + TdsError::DimensionMismatch { + expected: 4, + actual: 3, + .. + } + )); + } + + // ========================================================================= + // REMOVE VERTEX: RETRIANGULATION AND TOPOLOGY + // ========================================================================= + + #[test] + fn test_remove_vertex_retriangulates_cavity_2d() { + // Build 2D triangulation with 4 vertices, remove one, verify valid. + let vertices = [ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([0.5, 0.5]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + let initial_simplices = dt.number_of_simplices(); + let vertex_key = dt + .vertices() + .find(|(_, v)| { + let c = v.point().coords(); + (c[0] - 0.5).abs() < 1e-10 && (c[1] - 0.5).abs() < 1e-10 + }) + .map(|(k, _)| k) + .unwrap(); + + let removed = dt.remove_vertex(vertex_key).unwrap(); + assert!(removed > 0, "Should have removed at least 1 simplex"); + assert!(dt.number_of_simplices() <= initial_simplices); + assert_eq!(dt.number_of_vertices(), 3); + } + + #[test] + fn test_remove_vertex_entire_triangulation_2d() { + // When we remove a vertex from a single-simplex triangulation, + // the empty boundary case triggers Tds::remove_vertex fallback. + let vertices = [ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + let vertex_key = dt.vertices().next().unwrap().0; + let removed = dt.remove_vertex(vertex_key).unwrap(); + assert!(removed >= 1); + assert_eq!(dt.number_of_vertices(), 2); + } + + // ========================================================================= + // FAN FILL CAVITY: ERROR CASE + // ========================================================================= + + #[test] + fn test_fan_fill_cavity_errors_when_no_simplices_produced() { + // If the apex is on every boundary facet, fan_fill_cavity should error. + let (mut tri, vkeys, ck) = build_single_tet(); + + // Use vkeys[0] as apex; construct boundary facets that ALL include vkeys[0]. + // In a tet, facet 0 is opposite vkeys[0] (does NOT include it), + // but facets 1,2,3 each include vkeys[0]. + let boundary_facets: CavityBoundaryBuffer = + (1..=3).map(|i| FacetHandle::new(ck, i)).collect(); + + let result = tri.fan_fill_cavity(vkeys[0], &boundary_facets); + // All facets include vkeys[0], so no simplices should be created. + assert!(result.is_err()); + } + + // ========================================================================= + // REPAIR LOCAL FACET ISSUES: NON-EMPTY ISSUES MAP + // ========================================================================= + + #[test] + fn test_repair_local_facet_issues_handles_duplicate_simplex_fixture_transactionally() { + // Build 2D triangulation with enough simplices to have interior facets, + // then artificially create an over-shared facet by duplicating a simplex. + let vertices = [ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([1.0, 1.0]), + ]; + let dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + let mut tri = dt.as_triangulation().clone(); + + // Add a duplicate simplex with the same vertices as an existing simplex. + let (_, existing_simplex) = tri.tds.simplices().next().unwrap(); + let vkeys: Vec<_> = existing_simplex.vertices().to_vec(); + let dup_simplex = Simplex::new(vkeys, None).unwrap(); + let _ = tri + .tds + .insert_simplex_bypassing_topology_checks_for_test(dup_simplex) + .unwrap(); + + // Now detect issues. + let all_simplices: Vec<_> = tri.tds.simplex_keys().collect(); + let issues = tri.detect_local_facet_issues(&all_simplices).unwrap(); + assert!(issues.is_some(), "Should detect over-shared facet"); + let original_simplex_count = tri.tds.number_of_simplices(); + + match tri.repair_local_facet_issues(&issues.unwrap()) { + Ok(removed) => { + assert!(removed > 0, "repair should remove at least one simplex"); + tri.validate() + .expect("successful public repair should leave valid topology"); + } + Err(InsertionError::TopologyValidation(TdsError::DuplicateSimplices { .. })) => { + assert_eq!(tri.tds.number_of_simplices(), original_simplex_count); + } + Err(error) => panic!("unexpected duplicate-simplex repair error: {error:?}"), + } + } + + /// Return the facet index opposite the vertex not on the tested shared edge. + fn shared_edge_facet_index( + simplex: &Simplex, + v_a: VertexKey, + v_b: VertexKey, + ) -> usize { + simplex + .vertices() + .iter() + .position(|&vertex_key| vertex_key != v_a && vertex_key != v_b) + .expect("test simplices should contain the shared edge") + } + + /// Read the neighbor slot across the tested shared edge in a 2D repair fixture. + fn neighbor_across_shared_edge( + tri: &Triangulation, (), (), 2>, + simplex_key: SimplexKey, + v_a: VertexKey, + v_b: VertexKey, + ) -> Option { + let simplex = tri.tds.simplex(simplex_key).unwrap(); + let facet_idx = shared_edge_facet_index(simplex, v_a, v_b); + simplex.neighbor_key(facet_idx).flatten() + } + + #[test] + fn test_local_repair_uses_removal_frontier() { + let (mut tri, original_simplices, v_a, v_b) = build_overshared_edge_fixture(); + let issues = tri + .detect_local_facet_issues(&original_simplices) + .unwrap() + .expect("three simplices sharing one edge should be detected as over-shared"); + + let repair = tri + .repair_local_facet_issues_with_frontier(&issues) + .unwrap(); + assert_eq!(repair.removed_count, 1); + assert!( + !repair.frontier_simplices.is_empty(), + "removed-simplex neighbors should seed the local repair frontier" + ); + + let survivors: Vec<_> = original_simplices + .into_iter() + .filter(|simplex_key| tri.tds.contains_simplex(*simplex_key)) + .collect(); + assert_eq!(survivors.len(), 2); + let [first_survivor, second_survivor] = survivors.as_slice() else { + panic!("fixture should leave exactly two surviving simplices"); + }; + for &survivor in &survivors { + assert!( + repair.frontier_simplices.contains(&survivor), + "facet-issue survivors should seed the local repair frontier" + ); + } + let survivor_pairs = [ + (*first_survivor, *second_survivor), + (*second_survivor, *first_survivor), + ]; + + let missing_shared_slots_before = survivor_pairs + .iter() + .filter(|&&(simplex_key, other)| { + neighbor_across_shared_edge(&tri, simplex_key, v_a, v_b) != Some(other) + }) + .count(); + assert!( + missing_shared_slots_before > 0, + "simplex removal should leave at least one survivor slot needing local repair" + ); + + let mut new_simplices = SimplexKeyBuffer::new(); + new_simplices.extend(original_simplices); + let repaired = tri + .repair_neighbors_after_local_simplex_removal( + &new_simplices, + &repair.frontier_simplices, + ) + .unwrap(); + + assert!(repaired > 0); + for (simplex_key, other) in survivor_pairs { + assert_eq!( + neighbor_across_shared_edge(&tri, simplex_key, v_a, v_b), + Some(other), + "surviving simplices should be rewired across the formerly over-shared edge" + ); + } + assert!(tri.tds.validate_facet_sharing().is_ok()); + assert!(tri.detect_local_facet_issues(&survivors).unwrap().is_none()); + } + + #[test] + fn test_repair_local_facet_issues_rolls_back_invalid_public_repair() { + let (mut tri, original_simplices, _, _) = build_overshared_edge_fixture(); + let issues = tri + .detect_local_facet_issues(&original_simplices) + .unwrap() + .expect("three simplices sharing one edge should be detected as over-shared"); + let original_simplex_count = tri.tds.number_of_simplices(); + let original_vertex_count = tri.tds.number_of_vertices(); + + let result = tri.repair_local_facet_issues(&issues); + + assert!( + result.is_err(), + "public repair should reject an end state that fails full validation" + ); + assert_eq!(tri.tds.number_of_simplices(), original_simplex_count); + assert_eq!(tri.tds.number_of_vertices(), original_vertex_count); + for simplex_key in original_simplices { + assert!( + tri.tds.contains_simplex(simplex_key), + "rollback should restore every pre-repair simplex" + ); + } + } +} diff --git a/src/core/simplex.rs b/src/core/simplex.rs index 5a53891a..88e75c87 100644 --- a/src/core/simplex.rs +++ b/src/core/simplex.rs @@ -18,7 +18,7 @@ //! # Examples //! //! ```rust -//! use delaunay::prelude::triangulation::*; +//! use delaunay::prelude::*; //! //! // Create vertices for a tetrahedron //! let vertices = vec![ @@ -296,7 +296,7 @@ impl NeighborSlot { /// Since simplices store keys, use the owning [`Tds`] to resolve vertex data: /// ```rust /// use delaunay::prelude::collections::Uuid; -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// // Create a triangulation with some vertices /// let vertices = vec![ @@ -557,7 +557,7 @@ impl Simplex { /// # Example /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0]), @@ -592,7 +592,7 @@ impl Simplex { /// # Example /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0]), @@ -627,7 +627,7 @@ impl Simplex { /// # Example /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0]), @@ -663,7 +663,7 @@ impl Simplex { /// # Example /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0]), @@ -728,7 +728,7 @@ impl Simplex { /// /// ```rust /// use delaunay::prelude::tds::NeighborSlot; - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0]), @@ -766,7 +766,7 @@ impl Simplex { /// This method returns keys (not full vertex objects). Use the TDS to resolve keys: /// ```rust /// use delaunay::prelude::collections::Uuid; - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0]), @@ -976,7 +976,7 @@ impl Simplex { /// # Example /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -1005,7 +1005,7 @@ impl Simplex { /// /// ``` /// use delaunay::prelude::collections::Uuid; - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 1.0]), @@ -1028,7 +1028,7 @@ impl Simplex { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// /// let vertices = [ @@ -1087,7 +1087,7 @@ impl Simplex { /// # Example /// /// ``` - /// # use delaunay::prelude::triangulation::*; + /// # use delaunay::prelude::*; /// # let vertices = vec![ /// # vertex!([0.0, 0.0]), /// # vertex!([1.0, 0.0]), @@ -1126,7 +1126,7 @@ impl Simplex { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -1176,7 +1176,7 @@ impl Simplex { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0]), @@ -1211,7 +1211,7 @@ impl Simplex { /// # Example /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// /// let vertices = vec![ @@ -1248,7 +1248,7 @@ impl Simplex { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// /// // Create two separate triangulations @@ -1330,7 +1330,7 @@ impl Simplex { /// # Example /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// /// let vertices = vec![ @@ -1463,7 +1463,7 @@ impl Simplex { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// /// let vertices = vec![ @@ -1529,7 +1529,7 @@ impl Simplex { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// // Example 1: Comparing simplices from different TDS instances with same coordinates /// let vertices = vec![ @@ -1550,7 +1550,7 @@ impl Simplex { /// ``` /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// // Example 2: Comparing simplices with different coordinates returns false /// let vertices1 = vec![ @@ -1640,7 +1640,7 @@ impl Simplex { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// /// let vertices = vec![ @@ -1666,7 +1666,7 @@ impl Simplex { /// ``` /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// use delaunay::prelude::tds::Simplex; /// /// let vertices = vec![ @@ -1796,8 +1796,12 @@ impl Hash for Simplex { #[cfg(test)] mod tests { use super::*; + use crate::builder::DelaunayTriangulationBuilder; + use crate::construction::{ + ConstructionOptions, InitialSimplexStrategy, InsertionOrderStrategy, + }; use crate::core::facet::FacetError; - use crate::core::triangulation::TopologyGuarantee; + use crate::core::validation::TopologyGuarantee; use crate::core::vertex::vertex; use crate::geometry::kernel::AdaptiveKernel; use crate::geometry::matrix::MAX_STACK_MATRIX_DIM; @@ -1805,10 +1809,6 @@ mod tests { use crate::geometry::predicates::insphere; use crate::geometry::util::{circumcenter, circumradius, circumradius_with_center}; use crate::prelude::DelaunayTriangulation; - use crate::triangulation::builder::DelaunayTriangulationBuilder; - use crate::triangulation::delaunay::{ - ConstructionOptions, InitialSimplexStrategy, InsertionOrderStrategy, - }; use approx::assert_relative_eq; use std::{ cmp, diff --git a/src/core/tds.rs b/src/core/tds.rs index 05254c59..a903cd88 100644 --- a/src/core/tds.rs +++ b/src/core/tds.rs @@ -115,7 +115,7 @@ //! ## Example: Using Validation //! //! ```rust -//! use delaunay::prelude::triangulation::*; +//! use delaunay::prelude::*; //! //! let vertices = [ //! vertex!([0.0, 0.0, 0.0]), @@ -148,15 +148,15 @@ //! [`Simplex::is_valid()`]: crate::core::simplex::Simplex::is_valid //! [`Vertex::is_valid()`]: crate::core::vertex::Vertex::is_valid //! [`Triangulation::is_valid()`]: crate::core::triangulation::Triangulation::is_valid -//! [`DelaunayTriangulation::is_valid()`]: crate::triangulation::delaunay::DelaunayTriangulation::is_valid -//! [`DelaunayTriangulation::validation_report()`]: crate::triangulation::delaunay::DelaunayTriangulation::validation_report +//! [`DelaunayTriangulation::is_valid()`]: crate::DelaunayTriangulation::is_valid +//! [`DelaunayTriangulation::validation_report()`]: crate::DelaunayTriangulation::validation_report //! //! # Examples //! //! ## Creating a 3D Triangulation //! //! ```rust -//! use delaunay::prelude::triangulation::*; +//! use delaunay::prelude::*; //! //! // Create vertices for a tetrahedron //! let vertices = [ @@ -179,7 +179,7 @@ //! ## Adding Vertices to Existing Triangulation //! //! ```rust -//! use delaunay::prelude::triangulation::*; +//! use delaunay::prelude::*; //! //! // Start with initial vertices //! let initial_vertices = [ @@ -202,7 +202,7 @@ //! ## 4D Triangulation //! //! ```rust -//! use delaunay::prelude::triangulation::*; +//! use delaunay::prelude::*; //! //! // Create 4D triangulation with 5 vertices (needed for a 4-simplex) //! let vertices_4d = [ @@ -245,9 +245,9 @@ use crate::core::collections::{ UuidToSimplexKeyMap, UuidToVertexKeyMap, VertexKeyBuffer, VertexKeySet, fast_hash_map_with_capacity, }; -use crate::core::triangulation::TriangulationValidationError; +use crate::core::validation::TriangulationValidationError; use crate::geometry::traits::coordinate::CoordinateScalar; -use crate::triangulation::delaunay::DelaunayTriangulationValidationError; +use crate::validation::DelaunayTriangulationValidationError; use serde::{ Deserialize, Deserializer, Serialize, de::{self, MapAccess, Visitor}, @@ -405,7 +405,7 @@ pub enum GeometricError { /// Example usage /// /// ``` -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// // Build a simple 3D triangulation /// let vertices = [ @@ -1394,7 +1394,7 @@ pub struct InvariantViolation { /// [`DelaunayTriangulation::validation_report()`] /// to surface all failed invariants at once for debugging and test diagnostics. /// -/// [`DelaunayTriangulation::validation_report()`]: crate::triangulation::delaunay::DelaunayTriangulation::validation_report +/// [`DelaunayTriangulation::validation_report()`]: crate::DelaunayTriangulation::validation_report /// /// # Examples /// @@ -1436,7 +1436,7 @@ new_key_type! { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -1461,7 +1461,7 @@ new_key_type! { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -1509,14 +1509,14 @@ new_key_type! { /// /// `Tds` is the low-level topology container used by /// [`Triangulation`](crate::core::triangulation::Triangulation) and -/// [`DelaunayTriangulation`](crate::triangulation::delaunay::DelaunayTriangulation). +/// [`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. /// /// ```rust -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// // Create vertices for a 2D triangulation /// let vertices = [ @@ -1959,7 +1959,7 @@ impl Tds { /// # Example /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0, 0.0]), @@ -1990,7 +1990,7 @@ impl Tds { /// # Example /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0, 0.0]), @@ -2027,7 +2027,7 @@ impl Tds { /// # Example /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -2053,7 +2053,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -2077,7 +2077,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -2101,7 +2101,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -2128,7 +2128,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -2358,7 +2358,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0, 0.0]), @@ -2810,7 +2810,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -2870,7 +2870,7 @@ impl Tds { /// Successfully finding a simplex key from a UUID: /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// // Create a triangulation with some vertices /// let vertices = [ @@ -2930,7 +2930,7 @@ impl Tds { /// Successfully finding a vertex key from a UUID: /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// // Create a triangulation with some vertices /// let vertices = [ @@ -2990,7 +2990,7 @@ impl Tds { /// Successfully getting a UUID from a simplex key: /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// // Create a triangulation with some vertices /// let vertices = [ @@ -3015,7 +3015,7 @@ impl Tds { /// Round-trip conversion between UUID and key: /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// // Create a triangulation with some vertices /// let vertices = [ @@ -3065,7 +3065,7 @@ impl Tds { /// Successfully getting a UUID from a vertex key: /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// // Create a triangulation with some vertices /// let vertices = [ @@ -3090,7 +3090,7 @@ impl Tds { /// Round-trip conversion between UUID and key: /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// // Create a triangulation with some vertices /// let vertices = [ @@ -3165,7 +3165,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -3217,7 +3217,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices: [Vertex; 3] = [ /// vertex!([0.0, 0.0], 10i32), @@ -3269,7 +3269,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -3316,7 +3316,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -3347,7 +3347,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -3397,7 +3397,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -3796,7 +3796,7 @@ impl Tds { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -3892,7 +3892,7 @@ impl Tds { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -4384,7 +4384,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -4463,7 +4463,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -4524,7 +4524,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -4583,7 +4583,7 @@ impl Tds { /// /// /// This function creates an empty triangulation with no vertices and no simplices. - /// Use [`DelaunayTriangulation::empty()`](crate::triangulation::delaunay::DelaunayTriangulation::empty) + /// Use [`DelaunayTriangulation::empty()`](crate::DelaunayTriangulation::empty) /// for the high-level API, or this method for low-level Tds construction. /// /// # Returns @@ -4635,7 +4635,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0, 0.0]), @@ -4933,7 +4933,7 @@ impl Tds { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -5093,7 +5093,7 @@ impl Tds { /// /// This corresponds to [`InvariantKind::VertexMappings`], which is included in /// [`Tds::is_valid`](Self::is_valid) and [`Tds::validate`](Self::validate), and is also surfaced by - /// [`DelaunayTriangulation::validation_report()`](crate::triangulation::delaunay::DelaunayTriangulation::validation_report). + /// [`DelaunayTriangulation::validation_report()`](crate::DelaunayTriangulation::validation_report). /// /// # Errors /// @@ -5161,7 +5161,7 @@ impl Tds { /// [`Tds::is_valid`](Self::is_valid) and [`Tds::validate`](Self::validate), and is also surfaced by /// [`DelaunayTriangulation::validation_report()`]. /// - /// [`DelaunayTriangulation::validation_report()`]: crate::triangulation::delaunay::DelaunayTriangulation::validation_report + /// [`DelaunayTriangulation::validation_report()`]: crate::DelaunayTriangulation::validation_report /// /// # Errors /// @@ -5299,7 +5299,7 @@ impl Tds { /// [`Tds::is_valid`](Self::is_valid) and [`Tds::validate`](Self::validate), and is also surfaced by /// [`DelaunayTriangulation::validation_report()`]. /// - /// [`DelaunayTriangulation::validation_report()`]: crate::triangulation::delaunay::DelaunayTriangulation::validation_report + /// [`DelaunayTriangulation::validation_report()`]: crate::DelaunayTriangulation::validation_report fn validate_no_duplicate_simplices(&self) -> Result<(), TdsError> { // Include periodic per-vertex offsets in the duplicate key so periodic quotient simplices // with identical vertex sets but distinct lattice offsets are not collapsed. @@ -5413,7 +5413,7 @@ impl Tds { /// [`Tds::is_valid`](Self::is_valid) and [`Tds::validate`](Self::validate), and is also surfaced by /// [`DelaunayTriangulation::validation_report()`]. /// - /// [`DelaunayTriangulation::validation_report()`]: crate::triangulation::delaunay::DelaunayTriangulation::validation_report + /// [`DelaunayTriangulation::validation_report()`]: crate::DelaunayTriangulation::validation_report pub(crate) fn validate_facet_sharing(&self) -> Result<(), TdsError> { // Build a map from facet keys to the simplices that contain them. // Use the strict version to ensure we catch any missing vertex keys. @@ -5431,7 +5431,7 @@ impl Tds { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulation, DelaunayTriangulationConstructionError, vertex, /// }; /// @@ -5813,7 +5813,7 @@ impl Tds { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices_4d = [ /// vertex!([0.0, 0.0, 0.0, 0.0]), @@ -5866,7 +5866,7 @@ impl Tds { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices_4d = [ /// vertex!([0.0, 0.0, 0.0, 0.0]), @@ -6122,7 +6122,7 @@ impl Tds { /// [`Tds::is_valid`](Self::is_valid) and [`Tds::validate`](Self::validate), and is also surfaced by /// [`DelaunayTriangulation::validation_report()`]. /// - /// [`DelaunayTriangulation::validation_report()`]: crate::triangulation::delaunay::DelaunayTriangulation::validation_report + /// [`DelaunayTriangulation::validation_report()`]: crate::DelaunayTriangulation::validation_report /// /// Note: callers provide `facet_to_simplices` so `is_valid()` and `validation_report()` can share /// the precomputed facet map between validators. @@ -6899,21 +6899,21 @@ where #[cfg(test)] mod tests { use super::*; + use crate::builder::DelaunayTriangulationBuilder; use crate::core::algorithms::flips::DelaunayRepairError; use crate::core::algorithms::incremental_insertion::InsertionError; use crate::core::collections::NeighborBuffer; use crate::core::facet::FacetError; use crate::core::simplex::Simplex; - use crate::core::triangulation::TriangulationValidationError; use crate::core::util::uuid::UuidValidationError; + use crate::core::validation::TriangulationValidationError; use crate::core::vertex::VertexBuilder; use crate::geometry::point::Point; use crate::geometry::traits::coordinate::Coordinate; + use crate::repair::DelaunayRepairOperation; use crate::topology::characteristics::euler::TopologyClassification; - use crate::triangulation::builder::DelaunayTriangulationBuilder; - use crate::triangulation::delaunay::{ - DelaunayRepairOperation, DelaunayTriangulation, DelaunayTriangulationValidationError, - }; + use crate::triangulation::DelaunayTriangulation; + use crate::validation::DelaunayTriangulationValidationError; use crate::vertex; use slotmap::KeyData; use std::sync::Arc; diff --git a/src/core/traits/data_type.rs b/src/core/traits/data_type.rs index a73bdee2..e08ad2d3 100644 --- a/src/core/traits/data_type.rs +++ b/src/core/traits/data_type.rs @@ -64,7 +64,7 @@ impl DataSerde for T where T: DataSerialize + DataDeserialize {} /// # Usage /// /// ```rust -/// use delaunay::prelude::triangulation::DataType; +/// use delaunay::prelude::DataType; /// /// fn process_data(data: T) { /// // T has all the necessary bounds for use as vertex/simplex data diff --git a/src/core/traits/facet_cache.rs b/src/core/traits/facet_cache.rs index dfb9d6ee..4d545a11 100644 --- a/src/core/traits/facet_cache.rs +++ b/src/core/traits/facet_cache.rs @@ -37,7 +37,7 @@ use std::sync::{ /// use delaunay::prelude::tds::Tds; /// use delaunay::prelude::collections::FacetToSimplicesMap; /// use delaunay::prelude::geometry::CoordinateScalar; -/// use delaunay::prelude::triangulation::DataType; +/// use delaunay::prelude::DataType; /// use std::sync::Arc; /// use std::sync::atomic::{AtomicU64, Ordering}; /// use serde::de::DeserializeOwned; @@ -335,7 +335,7 @@ mod tests { use super::*; use crate::core::tds::Tds; use crate::geometry::kernel::AdaptiveKernel; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use std::sync::Arc; use std::sync::Barrier; diff --git a/src/core/triangulation.rs b/src/core/triangulation.rs index c725b857..fdbc0cd8 100644 --- a/src/core/triangulation.rs +++ b/src/core/triangulation.rs @@ -6,1135 +6,16 @@ //! //! This layer provides geometric operations while delegating topology to Tds. //! -//! # Validation Hierarchy -//! -//! The library provides **four levels** of validation, each building on the previous: -//! -//! ## Level 1: Element Validity -//! - **Methods**: [`Simplex::is_valid()`], [`Vertex::is_valid()`] -//! - **Checks**: Basic data integrity (coordinate validity, UUID presence, proper initialization) -//! - **Cost**: O(1) per element -//! -//! ## Level 2: TDS Structural Validity -//! - **Method**: [`Tds::is_valid()`] -//! - **Checks**: -//! - UUID ↔ Key mapping consistency -//! - No duplicate simplices (same vertex sets) -//! - Facet sharing invariant (≤2 simplices per facet) -//! - Neighbor consistency (mutual relationships) -//! - **Cost**: O(N×D²) where N = simplices, D = dimension -//! -//! Use [`Tds::validate()`] for cumulative Levels 1–2 (element + structural) validation. -//! -//! ## Level 3: Manifold Topology -//! - **Method**: [`Triangulation::is_valid()`](crate::core::triangulation::Triangulation::is_valid) -//! - **Checks**: -//! - **Codimension-1 manifoldness**: exactly 1 boundary simplex or 2 interior simplices per facet -//! - **Codimension-2 boundary manifoldness**: the boundary is closed ("no boundary of boundary") -//! - Connectedness (single connected component in the simplex neighbor graph) -//! - No isolated vertices (every vertex must be incident to at least one simplex) -//! - Euler characteristic (χ = V - E + F - C matches expected topology) -//! - **Cost**: O(N×D²) dominated by simplex counting -//! -//! Use [`Triangulation::validate()`](crate::core::triangulation::Triangulation::validate) for cumulative Levels 1–3. -//! -//! ## Level 4: Delaunay Property -//! - **Method**: [`DelaunayTriangulation::is_valid()`](crate::triangulation::delaunay::DelaunayTriangulation::is_valid) -//! - **Checks**: Empty circumsphere property (no vertex inside any simplex's circumsphere) -//! - **Cost**: O(N×V) where N = simplices, V = vertices -//! -//! Use [`DelaunayTriangulation::validate()`](crate::triangulation::delaunay::DelaunayTriangulation::validate) for cumulative Levels 1–4. -//! -//! ## Usage Guidelines -//! -//! ```rust -//! use delaunay::prelude::triangulation::*; -//! -//! 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::new(&vertices).unwrap(); -//! -//! // Level 2: structural only (fast) -//! assert!(dt.tds().is_valid().is_ok()); -//! -//! // Level 3: topology only (assumes structural validity) -//! assert!(dt.as_triangulation().is_valid().is_ok()); -//! -//! // Level 4: Delaunay property only (assumes Levels 1–3) -//! assert!(dt.is_valid().is_ok()); -//! -//! // Full cumulative validation (Levels 1–4) -//! assert!(dt.validate().is_ok()); -//! ``` -//! -//! **Performance**: Use Level 2 for most production validation. Reserve Level 3 for -//! tests/debug builds, and Level 4 for critical verification or debugging geometric issues. -//! -//! [`Simplex::is_valid()`]: crate::core::simplex::Simplex::is_valid -//! [`Vertex::is_valid()`]: crate::core::vertex::Vertex::is_valid -//! [`Tds::is_valid()`]: crate::core::tds::Tds::is_valid -//! [`Tds::validate()`]: crate::core::tds::Tds::validate -//! -//! ## Topology guarantees -//! -//! [`TopologyGuarantee`](crate::core::triangulation::TopologyGuarantee) selects which **manifoldness** -//! invariants are checked by Level 3 topology validation. -//! -//! Whether these checks run automatically after insertion is controlled by -//! [`ValidationPolicy`](crate::core::triangulation::ValidationPolicy). -//! -//! Level 3 validation always checks: -//! - Codimension-1 facet degree (pseudomanifold condition: 1 boundary or 2 interior simplices per facet) -//! - Codimension-2 boundary manifoldness (closed boundary: "no boundary of boundary") -//! - Connectedness (single connected component in the simplex neighbor graph) -//! - No isolated vertices (every vertex must be incident to at least one simplex) -//! - Euler characteristic -//! -//! With [`TopologyGuarantee::PLManifold`](crate::core::triangulation::TopologyGuarantee::PLManifold), -//! Level 3 validation additionally checks the canonical **vertex-link** PL-manifoldness -//! condition via [`crate::topology::manifold::validate_vertex_links`]. -//! -//! Note: for **D=3**, the current vertex-link validator additionally enforces that each link -//! has the Euler characteristic / boundary component counts of a sphere/ball (S²/B²). -//! For **D≥4**, it currently checks that each vertex link is a connected (D−1)-manifold -//! with the correct boundary behavior (a necessary condition), but does not attempt to -//! distinguish spheres/balls from other manifolds (not sufficient in general). +//! Validation policy, topology guarantees, and validation passes are implemented +//! in [`crate::core::validation`]. //! #![forbid(unsafe_code)] -use crate::core::adjacency::{AdjacencyIndex, AdjacencyIndexBuildError}; -use crate::core::algorithms::incremental_insertion::{ - CavityFillingError, CavityRepairStage, HullExtensionReason, InsertionError, extend_hull, - external_facets_for_boundary, fill_cavity_replacing_simplices, repair_neighbor_pointers, - repair_neighbor_pointers_local, wire_cavity_neighbors, -}; -#[cfg(debug_assertions)] -use crate::core::algorithms::locate::locate; -#[cfg(feature = "diagnostics")] -use crate::core::algorithms::locate::verify_conflict_region_completeness; -use crate::core::algorithms::locate::{ - ConflictError, LocateError, LocateResult, LocateStats, extract_cavity_boundary, - find_conflict_region, locate_by_scan, locate_with_stats, locate_with_trace, -}; -use crate::core::collections::spatial_hash_grid::HashGridIndex; -use crate::core::collections::{ - CavityBoundaryBuffer, FacetIssuesMap, FacetToSimplicesMap, FastHashMap, FastHashSet, - FastHasher, MAX_PRACTICAL_DIMENSION_SIZE, SimplexKeyBuffer, SimplexKeySet, SmallBuffer, - VertexToSimplicesMap, fast_hash_map_with_capacity, fast_hash_set_with_capacity, -}; -use crate::core::edge::EdgeKey; -#[cfg(test)] -use crate::core::facet::facet_key_from_vertices; -use crate::core::facet::{AllFacetsIter, BoundaryFacetsIter, FacetHandle}; -use crate::core::operations::{ - InsertionOutcome, InsertionResult, InsertionStatistics, InsertionTelemetry, - InsertionTelemetryMode, SuspicionFlags, -}; -use crate::core::simplex::{Simplex, SimplexValidationError}; -#[cfg(test)] -use crate::core::tds::NeighborValidationError; -use crate::core::tds::{ - GeometricError, InvariantError, InvariantErrorSummary, InvariantKind, InvariantViolation, - SimplexKey, Tds, TdsConstructionError, TdsError, TriangulationValidationReport, VertexKey, -}; -use crate::core::traits::data_type::DataType; -#[cfg(test)] -use crate::core::util::canonical_points::sorted_simplex_points; -use crate::core::vertex::Vertex; +use crate::core::tds::{SimplexKey, Tds, VertexKey}; +use crate::core::validation::{TopologyGuarantee, ValidationPolicy}; use crate::geometry::kernel::Kernel; -use crate::geometry::point::Point; -use crate::geometry::predicates::Orientation; -use crate::geometry::quality::radius_ratio; -use crate::geometry::robust_predicates::robust_orientation; -use crate::geometry::traits::coordinate::{Coordinate, CoordinateScalar}; -use crate::geometry::util::safe_scalar_to_f64; -use crate::topology::characteristics::euler::{TopologyClassification, expected_chi_for}; -use crate::topology::characteristics::validation::validate_triangulation_euler_with_facet_to_simplices_map; -use crate::topology::manifold::{ - ManifoldError, validate_closed_boundary, validate_facet_degree, - validate_local_pseudomanifold_for_simplices, validate_ridge_links, - validate_ridge_links_for_simplices, validate_vertex_links, -}; -use crate::topology::traits::global_topology_model::GlobalTopologyModel; -use crate::topology::traits::topological_space::{GlobalTopology, TopologyKind}; -use crate::triangulation::delaunay::DelaunayTriangulationValidationError; -use crate::triangulation::locality::{ - append_live_unique_simplex_seeds, collect_local_exterior_conflict_seed_simplices, - replace_simplices_and_record_removed, retain_simplices_and_record_removed, -}; -use core::ops::Div; -use num_traits::{Float, NumCast, One, Zero}; -use std::borrow::Cow; -#[cfg(all(test, debug_assertions))] -use std::cmp::Ordering as CmpOrdering; -use std::env; -use std::fmt::Write as _; -use std::hash::{Hash, Hasher}; -use std::sync::{ - OnceLock, - atomic::{AtomicBool, AtomicU64, Ordering}, -}; -use std::time::{Duration, Instant}; -use thiserror::Error; -use uuid::Uuid; - -/// Maximum number of repair iterations for fixing non-manifold topology after insertion. -/// -/// This limit prevents infinite loops in the rare case where repair cannot make progress. -/// In practice, most insertions require 0-2 iterations to restore manifold topology. -const MAX_REPAIR_ITERATIONS: usize = 10; - -/// Default number of perturbation retries for transactional insertion. -/// -/// Each retry uses a progressively larger perturbation magnitude (×10 per attempt), -/// so 3 retries span 4 orders of magnitude (e.g. `1e-8` → `1e-5` × `local_scale` for f64). -const DEFAULT_PERTURBATION_RETRIES: usize = 3; - -/// Telemetry: counts how often the topology safety-net recovered from a Level 3 validation -/// failure by retrying insertion with a star-split of the containing simplex. -/// -/// This is a process-wide counter across all triangulation instances. -/// -/// This counter is intentionally lightweight and can be polled by production workloads -/// to see whether this recovery path is frequently used. -static TOPOLOGY_SAFETY_NET_STAR_SPLIT_FALLBACK_SUCCESSES: AtomicU64 = AtomicU64::new(0); -static DUPLICATE_DETECTION_TOTAL: AtomicU64 = AtomicU64::new(0); -static DUPLICATE_DETECTION_GRID_USED: AtomicU64 = AtomicU64::new(0); -static DUPLICATE_DETECTION_GRID_FALLBACKS: AtomicU64 = AtomicU64::new(0); -static DUPLICATE_DETECTION_GRID_CANDIDATES: AtomicU64 = AtomicU64::new(0); -static DUPLICATE_DETECTION_ENABLED: OnceLock = OnceLock::new(); -static RETRYABLE_SKIP_TRACE_ENABLED: OnceLock = OnceLock::new(); -static CAVITY_REDUCTION_TRACE_ENABLED: OnceLock = OnceLock::new(); -static FORCE_GLOBAL_NEIGHBOR_REBUILD_ENABLED: OnceLock = OnceLock::new(); -static CAVITY_REDUCTION_TRACE_EMITTED: AtomicBool = AtomicBool::new(false); - -#[cfg(test)] -static DUPLICATE_DETECTION_FORCE_ENABLED: AtomicBool = AtomicBool::new(false); - -#[cfg(debug_assertions)] -static VERTEX_TO_SIMPLICES_SPILL_EVENTS: AtomicU64 = AtomicU64::new(0); - -#[cfg(test)] -mod test_hooks { - use std::cell::Cell; - - thread_local! { - static FORCE_NEXT_INSERTION_RETRYABLE_FAILURE: Cell = const { Cell::new(false) }; - } - - pub(super) fn take_force_next_insertion_retryable_failure() -> bool { - FORCE_NEXT_INSERTION_RETRYABLE_FAILURE.replace(false) - } - - pub(super) fn set_force_next_insertion_retryable_failure(enabled: bool) -> bool { - FORCE_NEXT_INSERTION_RETRYABLE_FAILURE.replace(enabled) - } - - pub(super) fn restore_force_next_insertion_retryable_failure(prior: bool) { - FORCE_NEXT_INSERTION_RETRYABLE_FAILURE.set(prior); - } -} - -fn duplicate_detection_metrics_enabled() -> bool { - #[cfg(test)] - if DUPLICATE_DETECTION_FORCE_ENABLED.load(Ordering::Relaxed) { - return true; - } - *DUPLICATE_DETECTION_ENABLED.get_or_init(|| env::var_os("DELAUNAY_DUPLICATE_METRICS").is_some()) -} - -/// Caches whether retryable conflict-region skips should emit release-visible traces. -fn retryable_skip_trace_enabled() -> bool { - *RETRYABLE_SKIP_TRACE_ENABLED - .get_or_init(|| env::var_os("DELAUNAY_DEBUG_RETRYABLE_SKIP").is_some()) -} - -/// Returns whether the first cavity-reduction chain should emit release-visible tracing. -fn cavity_reduction_trace_enabled() -> bool { - *CAVITY_REDUCTION_TRACE_ENABLED - .get_or_init(|| env::var_os("DELAUNAY_DEBUG_CAVITY_REDUCTION_ONCE").is_some()) -} - -/// Returns whether local neighbor repair should be bypassed for regression isolation. -fn force_global_neighbor_rebuild_enabled() -> bool { - *FORCE_GLOBAL_NEIGHBOR_REBUILD_ENABLED - .get_or_init(|| env::var_os("DELAUNAY_FORCE_GLOBAL_NEIGHBOR_REBUILD").is_some()) -} - -/// Extracts a compact one-line summary for retryable conflict-region failures. -/// -/// These summaries are designed for the large-scale debug harness logs, where we want -/// enough structure to correlate repeated ridge-fan failures without dumping the entire -/// conflict region. -fn retryable_conflict_trace_detail(error: &InsertionError) -> Option { - match error { - InsertionError::ConflictRegion(ConflictError::NonManifoldFacet { - facet_hash, - simplex_count, - }) => Some(format!( - "kind=non_manifold_facet facet_hash={facet_hash:#x} simplex_count={simplex_count}" - )), - InsertionError::ConflictRegion(ConflictError::RidgeFan { - facet_count, - ridge_vertex_count, - extra_simplices, - }) => Some(format!( - "kind=ridge_fan facet_count={facet_count} ridge_vertex_count={ridge_vertex_count} \ - extra_simplices={}", - extra_simplices.len() - )), - InsertionError::ConflictRegion(ConflictError::DisconnectedBoundary { - visited, - total, - disconnected_simplices, - }) => Some(format!( - "kind=disconnected_boundary visited={visited} total={total} disconnected_simplices={}", - disconnected_simplices.len() - )), - InsertionError::ConflictRegion(ConflictError::OpenBoundary { - facet_count, - ridge_vertex_count, - .. - }) => Some(format!( - "kind=open_boundary facet_count={facet_count} ridge_vertex_count={ridge_vertex_count}" - )), - _ => None, - } -} - -/// Formats a compact summary for cavity-boundary extraction failures. -fn cavity_conflict_error_summary(error: &ConflictError) -> String { - match error { - ConflictError::NonManifoldFacet { - facet_hash, - simplex_count, - } => format!("non_manifold_facet facet_hash={facet_hash:#x} simplex_count={simplex_count}"), - ConflictError::RidgeFan { - facet_count, - ridge_vertex_count, - extra_simplices, - } => format!( - "ridge_fan facet_count={facet_count} ridge_vertex_count={ridge_vertex_count} \ - extra_simplices={}", - extra_simplices.len() - ), - ConflictError::DisconnectedBoundary { - visited, - total, - disconnected_simplices, - } => format!( - "disconnected_boundary visited={visited} total={total} disconnected_simplices={}", - disconnected_simplices.len() - ), - ConflictError::OpenBoundary { - facet_count, - ridge_vertex_count, - open_simplex, - } => format!( - "open_boundary facet_count={facet_count} ridge_vertex_count={ridge_vertex_count} \ - open_simplex={open_simplex:?}" - ), - ConflictError::InvalidStartSimplex { simplex_key } => { - format!("invalid_start_simplex simplex_key={simplex_key:?}") - } - ConflictError::PredicateError { source } => { - format!("predicate_error source={source}") - } - ConflictError::SimplexDataAccessFailed { - simplex_key, - message, - } => { - format!("simplex_data_access_failed simplex_key={simplex_key:?} message={message}") - } - ConflictError::InternalInconsistency { site } => { - format!("internal_inconsistency site={site}") - } - } -} - -/// Emits one-shot tracing for the first cavity-reduction chain in a run. -/// -/// Routed through `tracing::debug!`; enable with `RUST_LOG=debug` (the -/// large-scale debug harness wires this up automatically when -/// `DELAUNAY_DEBUG_CAVITY_REDUCTION_ONCE` is set). -fn log_cavity_reduction_event( - enabled: bool, - iteration: usize, - conflict_simplices: &SimplexKeyBuffer, - event: F, -) where - F: FnOnce() -> String, -{ - if !enabled { - return; - } - - let conflict_preview: Vec = conflict_simplices.iter().copied().take(12).collect(); - let event = event(); - tracing::debug!( - target: "delaunay::cavity_reduction", - iteration, - conflict_simplices = conflict_simplices.len(), - event, - conflict_preview = ?conflict_preview, - "cavity-reduction event" - ); -} - -#[expect( - clippy::too_many_arguments, - reason = "Diagnostic helper keeps retryable skip instrumentation centralized" -)] -/// Emits a single structured line for a retryable conflict-region skip after rollback. -/// -/// Logging after rollback lets the trace report both the state we tried to modify and -/// the restored simplex/vertex counts that future attempts will see. Routed through -/// `tracing::debug!` so callers can filter it via `RUST_LOG`; enabled for release-mode -/// runs by `DELAUNAY_DEBUG_RETRYABLE_SKIP`. -fn log_retryable_conflict_skip( - bulk_index: Option, - uuid: Uuid, - attempt: usize, - max_attempts: usize, - used_perturbation: bool, - will_retry: bool, - simplices_before_attempt: usize, - vertices_before_attempt: usize, - simplices_after_rollback: usize, - vertices_after_rollback: usize, - detail: &str, - error: &InsertionError, -) { - if !retryable_skip_trace_enabled() { - return; - } - - let bulk_index_display = bulk_index.map_or_else(|| String::from("n/a"), |idx| idx.to_string()); - tracing::debug!( - target: "delaunay::retryable_skip", - bulk_index = %bulk_index_display, - uuid = %uuid, - attempt, - max_attempts, - used_perturbation, - rolled_back = true, - will_retry, - simplices_before_attempt, - vertices_before_attempt, - simplices_after_rollback, - vertices_after_rollback, - conflict = %detail, - error = %error, - "retryable conflict-region skip after rollback" - ); -} - -/// Telemetry counters for duplicate-coordinate detection. -#[must_use] -#[derive(Debug, Clone, Copy, Default)] -pub struct DuplicateDetectionMetrics { - /// Total number of duplicate-coordinate checks executed. - pub total_checks: u64, - /// Number of checks that successfully used the hash grid. - pub grid_used: u64, - /// Number of checks that fell back to a non-grid scan. - pub grid_fallbacks: u64, - /// Total candidate vertices inspected during grid-based checks. - pub grid_candidates: u64, -} - -pub(crate) fn record_duplicate_detection_metrics( - used_grid: bool, - candidate_count: usize, - fell_back: bool, -) { - if !duplicate_detection_metrics_enabled() { - return; - } - DUPLICATE_DETECTION_TOTAL.fetch_add(1, Ordering::Relaxed); - if used_grid { - DUPLICATE_DETECTION_GRID_USED.fetch_add(1, Ordering::Relaxed); - DUPLICATE_DETECTION_GRID_CANDIDATES.fetch_add(candidate_count as u64, Ordering::Relaxed); - } - if fell_back { - DUPLICATE_DETECTION_GRID_FALLBACKS.fetch_add(1, Ordering::Relaxed); - } -} - -/// Convert an [`InsertionError`] into the appropriate [`InvariantError`], preserving -/// structured error information across all layers. -/// -/// - `TopologyValidation(source)` → `InvariantError::Tds(source)` (Level 1–2 preserved) -/// - `TopologyValidationFailed { source }` → `InvariantError::Triangulation(source)` (Level 3 preserved) -/// - All other variants → `InvariantError::Tds(InconsistentDataStructure { .. })` with `context` -pub(crate) fn insertion_error_to_invariant_error( - error: InsertionError, - context: &str, -) -> InvariantError { - match error { - InsertionError::TopologyValidation(source) => InvariantError::Tds(source), - InsertionError::TopologyValidationFailed { source, .. } => { - InvariantError::Triangulation(source) - } - other => InvariantError::Tds(TdsError::InconsistentDataStructure { - message: format!("{context}: {other}"), - }), - } -} - -/// Errors that can occur during triangulation construction. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::{Triangulation, TriangulationConstructionError}; -/// use delaunay::prelude::geometry::FastKernel; -/// use delaunay::vertex; -/// -/// let vertices = vec![ -/// vertex!([0.0, 0.0]), -/// vertex!([1.0, 0.0]), -/// vertex!([0.0, 1.0]), -/// ]; -/// let result: Result<_, TriangulationConstructionError> = -/// Triangulation::, (), (), 2>::build_initial_simplex(&vertices); -/// assert!(result.is_ok()); -/// ``` -#[derive(Clone, Debug, Error, PartialEq, Eq)] -#[non_exhaustive] -pub enum TriangulationConstructionError { - /// Lower-layer construction error in the TDS. - #[error(transparent)] - Tds(#[from] TdsConstructionError), - - /// Failed to create a simplex during triangulation construction. - #[error("Failed to create simplex during construction: {message}")] - FailedToCreateSimplex { - /// Description of the simplex creation failure. - message: String, - }, - - /// Cavity filling failed during incremental construction. - #[error("Cavity filling failed during insertion: {source}")] - InsertionCavityFilling { - /// Underlying cavity-filling error. - #[source] - source: CavityFillingError, - }, - - /// Insufficient vertices to create a triangulation. - #[error("Insufficient vertices for {dimension}D triangulation: {source}")] - InsufficientVertices { - /// The dimension that was attempted. - dimension: usize, - /// The underlying simplex validation error. - source: SimplexValidationError, - }, - - /// Geometric degeneracy prevents triangulation construction. - #[error("Geometric degeneracy encountered during construction: {message}")] - GeometricDegeneracy { - /// Description of the degeneracy issue. - message: String, - }, - - /// Conflict-region extraction failed during incremental construction. - #[error("Conflict region failed during insertion: {source}")] - InsertionConflictRegion { - /// Underlying conflict-region error. - #[source] - source: ConflictError, - }, - - /// Point location failed during incremental construction. - #[error("Point location failed during insertion: {source}")] - InsertionLocation { - /// Underlying point-location error. - #[source] - source: LocateError, - }, - - /// Incremental insertion detected non-manifold topology. - #[error( - "Non-manifold topology during insertion: facet {facet_hash:#x} shared by {simplex_count} simplices" - )] - InsertionNonManifoldTopology { - /// Hash of the over-shared facet. - facet_hash: u64, - /// Number of simplices sharing the facet. - simplex_count: usize, - }, - - /// Hull extension failed during incremental construction. - #[error("Hull extension failed during insertion: {reason}")] - InsertionHullExtension { - /// Structured hull-extension failure reason. - reason: HullExtensionReason, - }, - - /// Level 4 Delaunay validation failed during incremental construction. - #[error("Delaunay validation failed during insertion: {source}")] - InsertionDelaunayValidation { - /// Underlying Delaunay validation error. - #[source] - source: DelaunayTriangulationValidationError, - }, - - /// Level 3 topology validation failed during incremental construction. - #[error("{message}: {source}")] - InsertionTopologyValidation { - /// High-level insertion context. - message: String, - /// Underlying topology validation error. - #[source] - source: TriangulationValidationError, - }, - - /// Final cumulative topology validation failed after construction. - /// - /// Mirrors [`InsertionTopologyValidation`](Self::InsertionTopologyValidation) - /// for post-build checks that run after the incremental insertion phase. - #[error("{message}: {source}")] - FinalTopologyValidation { - /// High-level finalization context. - message: String, - /// Underlying validation error. - #[source] - source: InvariantErrorSummary, - }, - - /// Attempted to insert a vertex with coordinates that already exist. - #[error( - "Duplicate coordinates: vertex with coordinates {coordinates} already exists in the triangulation" - )] - DuplicateCoordinates { - /// String representation of the duplicate coordinates. - coordinates: String, - }, - - /// Internal bookkeeping state became inconsistent during construction. - /// - /// This indicates a bug in the construction algorithm rather than invalid - /// input or geometric degeneracy. - #[error("Internal inconsistency during construction: {message}")] - InternalInconsistency { - /// Description of the inconsistency. - message: String, - }, -} - -/// Errors that can occur during triangulation topology validation (Level 3). -/// -/// This type represents **only** Level 3 (topology) errors. It does not contain -/// TDS-level (Levels 1–2) errors. Cumulative validators that can return errors -/// from any level use [`InvariantError`] instead. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::tds::InvariantError; -/// use delaunay::prelude::triangulation::*; -/// -/// 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::new(&vertices).unwrap(); -/// -/// let result: Result<(), InvariantError> = dt.as_triangulation().validate(); -/// assert!(result.is_ok()); -/// ``` -#[derive(Clone, Debug, Error, PartialEq, Eq)] -#[non_exhaustive] -pub enum TriangulationValidationError { - /// A facet belongs to an unexpected number of simplices for a manifold-with-boundary. - #[error( - "Non-manifold facet: facet {facet_key:016x} belongs to {simplex_count} simplices (expected 1 or 2)" - )] - ManifoldFacetMultiplicity { - /// The facet key with invalid multiplicity. - facet_key: u64, - /// The number of incident simplices observed. - simplex_count: usize, - }, - - /// Boundary is not a closed (D-1)-manifold: - /// wrong number of boundary facets. - /// - /// This detects "boundary of boundary" issues (codimension-2 manifoldness of the boundary). - #[error( - "Boundary is not closed: boundary ridge {ridge_key:016x} is incident to {boundary_facet_count} boundary facets (expected 2)" - )] - BoundaryRidgeMultiplicity { - /// Canonical key for the (D-2)-simplex (ridge) on the boundary. - ridge_key: u64, - /// Number of incident boundary facets observed. - boundary_facet_count: usize, - }, - - /// A ridge's link graph is not a 1-manifold (path or cycle). - /// - /// This is required for PL-manifold validation. - #[error( - "Ridge link is not a 1-manifold: ridge {ridge_key:016x} has link graph with {link_vertex_count} vertices, {link_edge_count} edges, max degree {max_degree}, degree-1 vertices {degree_one_vertices}, connected={connected} (expected connected cycle or path)" - )] - RidgeLinkNotManifold { - /// Canonical key for the (D-2)-simplex (ridge). - ridge_key: u64, - /// Number of vertices in the ridge's link graph. - link_vertex_count: usize, - /// Number of edges in the ridge's link graph. - link_edge_count: usize, - /// Maximum vertex degree observed in the link graph. - max_degree: usize, - /// Number of vertices of degree 1 observed in the link graph. - degree_one_vertices: usize, - /// Whether the link graph is connected. - connected: bool, - }, - - /// A vertex link is not a (D-1)-manifold (sphere/ball) as required for PL-manifoldness. - #[error( - "Vertex link is not a PL (D-1)-manifold: vertex {vertex_key:?} has link with {link_vertex_count} vertices, {link_simplex_count} simplices, boundary_facets={boundary_facet_count}, max_degree={max_degree}, connected={connected}, interior_vertex={interior_vertex}" - )] - VertexLinkNotManifold { - /// The vertex whose link failed validation. - vertex_key: VertexKey, - /// Number of vertices in the link (0-simplices of the link). - link_vertex_count: usize, - /// Number of (D-1)-simplices (simplices) in the link. - link_simplex_count: usize, - /// Number of boundary facets in the link (facets of degree 1). - boundary_facet_count: usize, - /// Maximum degree in the link 1-skeleton. - max_degree: usize, - /// Whether the link 1-skeleton is connected. - connected: bool, - /// Whether the vertex was classified as an interior vertex of the original complex. - interior_vertex: bool, - }, - - /// Euler characteristic does not match the expected value for the classified topology. - #[error( - "Euler characteristic mismatch: computed χ={computed}, expected χ={expected} for {classification:?}" - )] - EulerCharacteristicMismatch { - /// Computed Euler characteristic. - computed: isize, - /// Expected Euler characteristic for the classification. - expected: isize, - /// The topology classification used to determine expectation. - classification: TopologyClassification, - }, - - /// Vertex is not incident to any simplex. - /// - /// An isolated vertex violates manifold invariants at the topology (Level 3) layer - /// and may indicate a failed insertion or an insertion that was partially rolled back. - #[error( - "Isolated vertex: vertex {vertex_uuid} (key {vertex_key:?}) is not incident to any simplex" - )] - IsolatedVertex { - /// Key of the isolated vertex. - vertex_key: VertexKey, - /// UUID of the isolated vertex. - vertex_uuid: Uuid, - }, - - /// The simplex neighbor graph is not a single connected component. - /// - /// A valid triangulation-with-boundary must be connected; multiple disconnected - /// components indicate a structural problem (e.g. simplices that share only a vertex - /// or edge but no facet, so no neighbor pointers link them). - #[error( - "Disconnected triangulation: simplex neighbor graph is not a single connected component ({simplex_count} simplices total)" - )] - Disconnected { - /// Total number of simplices in the triangulation. - simplex_count: usize, - }, -} - -impl TryFrom for TriangulationValidationError { - type Error = TdsError; - - fn try_from(err: ManifoldError) -> Result { - match err { - ManifoldError::Tds(source) => Err(source), - ManifoldError::ManifoldFacetMultiplicity { - facet_key, - simplex_count, - } => Ok(Self::ManifoldFacetMultiplicity { - facet_key, - simplex_count, - }), - ManifoldError::BoundaryRidgeMultiplicity { - ridge_key, - boundary_facet_count, - } => Ok(Self::BoundaryRidgeMultiplicity { - ridge_key, - boundary_facet_count, - }), - ManifoldError::RidgeLinkNotManifold { - ridge_key, - link_vertex_count, - link_edge_count, - max_degree, - degree_one_vertices, - connected, - } => Ok(Self::RidgeLinkNotManifold { - ridge_key, - link_vertex_count, - link_edge_count, - max_degree, - degree_one_vertices, - connected, - }), - ManifoldError::VertexLinkNotManifold { - vertex_key, - link_vertex_count, - link_simplex_count, - boundary_facet_count, - max_degree, - connected, - interior_vertex, - } => Ok(Self::VertexLinkNotManifold { - vertex_key, - link_vertex_count, - link_simplex_count, - boundary_facet_count, - max_degree, - connected, - interior_vertex, - }), - } - } -} - -impl From for InvariantError { - fn from(err: ManifoldError) -> Self { - match TriangulationValidationError::try_from(err) { - Ok(source) => Self::Triangulation(source), - Err(source) => Self::Tds(source), - } - } -} - -struct TryInsertImplOk { - /// Inserted vertex key plus an optional locate hint for the caller. - inserted: (VertexKey, Option), - /// Number of simplices removed during local non-manifold repair. - simplices_removed: usize, - /// Suspicion flags observed during the insertion attempt. - suspicion: SuspicionFlags, - /// Simplices touched by insertion that should seed follow-up local repair. - /// - /// This includes live simplices created by the insertion plus simplices that were shrunk - /// out of the final conflict region so higher layers can revisit nearby - /// Delaunay violations without rediscovering the inserted vertex star globally. - repair_seed_simplices: SimplexKeyBuffer, - /// Whether the insertion path can leave local Delaunay work for the caller. - /// - /// Clean interior Bowyer-Watson insertions preserve the Delaunay property. - /// Exterior hull extensions and suspicious fallback/repair paths still need - /// a local flip-repair pass. - delaunay_repair_required: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum InsertionValidationWork { - FullValidation, - RequiredTopologyLinks, -} - -/// Internal result from over-shared-facet repair, including the surviving frontier -/// that should seed local neighbor-pointer repair. -struct LocalFacetRepairOutcome { - /// Number of simplices actually removed from the TDS. - removed_count: usize, - /// Simplices selected for removal before they were deleted. - #[cfg_attr( - not(debug_assertions), - expect( - dead_code, - reason = "Removed-simplex keys are retained for debug logging and future local repair diagnostics" - ) - )] - removed_simplices: SimplexKeyBuffer, - /// Surviving one-hop neighbors whose back-references may have been cleared. - frontier_simplices: SimplexKeyBuffer, -} - -/// Result of filling one insertion cavity, including the follow-up Delaunay -/// repair requirements that depend on how the cavity was shaped. -struct CavityInsertionOutcome { - /// Locate hint for the next insertion. - hint: Option, - /// Number of simplices removed during local non-manifold repair. - simplices_removed: usize, - /// Simplices touched by insertion that should seed follow-up local repair. - repair_seed_simplices: SimplexKeyBuffer, - /// Whether this cavity path can leave Delaunay work for the caller. - delaunay_repair_required: bool, -} - -enum InsertionSite<'a> { - Interior { - start_simplex: SimplexKey, - conflict_simplices: Cow<'a, SimplexKeyBuffer>, - }, - Exterior { - conflict_simplices: Option>, - repair_seed_simplices: SimplexKeyBuffer, - }, -} - -/// Internal insertion result that preserves the user-facing outcome plus -/// hidden repair seeding used by batch/debug construction paths. -pub(crate) struct DetailedInsertionResult { - /// Public insertion outcome returned to higher layers. - pub outcome: InsertionOutcome, - /// Public statistics collected while attempting the insertion. - pub stats: InsertionStatistics, - /// Internal path telemetry collected while attempting the insertion. - pub telemetry: InsertionTelemetry, - /// Local simplices that should seed the caller's Delaunay repair set. - pub repair_seed_simplices: SimplexKeyBuffer, - /// Whether callers should run Delaunay repair over `repair_seed_simplices`. - pub delaunay_repair_required: bool, -} - -/// Policy controlling when the triangulation runs global validation passes. -/// -/// Validation can be expensive (O(N×D²) or worse), so this allows callers to trade -/// performance for stricter correctness checks during incremental operations. -/// -/// **Note**: [`TopologyGuarantee::PLManifold`] is incompatible with [`ValidationPolicy::Never`]. -/// `PLManifold` requires at least end-of-construction validation to certify full -/// PL-manifoldness. Use [`ValidationPolicy::OnSuspicion`] (default) for best performance, -/// or [`ValidationPolicy::Always`] for maximum safety during incremental operations. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::operations::SuspicionFlags; -/// use delaunay::prelude::triangulation::ValidationPolicy; -/// -/// let policy = ValidationPolicy::OnSuspicion; -/// let suspicion = SuspicionFlags { perturbation_used: true, ..SuspicionFlags::default() }; -/// assert!(policy.should_validate(suspicion)); -/// ``` -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ValidationPolicy { - /// Never run global validation. - Never, - - /// Validate only if the operation is suspicious (e.g. degeneracy). - OnSuspicion, - - /// Always validate after insertion. - Always, - - /// Debug builds: always validate; release builds: [`ValidationPolicy::OnSuspicion`]. - DebugOnly, -} - -impl ValidationPolicy { - /// Returns `true` if a global validation pass should be run given the observed - /// [`crate::core::operations::SuspicionFlags`]. - #[inline] - #[must_use] - pub const fn should_validate(&self, suspicion: SuspicionFlags) -> bool { - match self { - Self::Never => false, - Self::Always => true, - Self::OnSuspicion => suspicion.is_suspicious(), - Self::DebugOnly => cfg!(debug_assertions) || suspicion.is_suspicious(), - } - } -} - -impl Default for ValidationPolicy { - #[inline] - fn default() -> Self { - Self::OnSuspicion - } -} - -/// Selects which topological invariants are checked by Level 3 validation. -/// -/// This enum specifies *what is checked* about the underlying simplicial complex when -/// Level 3 validation runs. Whether Level 3 validation runs automatically after insertion -/// is controlled by [`ValidationPolicy`]. -/// -/// - [`TopologyGuarantee::Pseudomanifold`] checks the codimension-1 adjacency condition: -/// each facet is incident to one or two simplices, and the codimension-2 boundary is closed. -/// This is sufficient for many geometric algorithms but does not guarantee local Euclidean structure. -/// -/// - [`TopologyGuarantee::PLManifold`] uses ridge-link validation during insertion and -/// requires a vertex-link validation pass at construction completion to certify -/// PL-manifoldness. -/// - [`TopologyGuarantee::PLManifoldStrict`] runs vertex-link validation after every -/// insertion for maximal safety (slowest). -/// -/// # Example -/// -/// ```rust -/// use delaunay::prelude::triangulation::*; -/// -/// 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 mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); -/// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); -/// -/// // Optional: relax topology checks for speed (weaker guarantees). -/// dt.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); -/// assert!(!dt.topology_guarantee().requires_vertex_links_at_completion()); -/// ``` -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum TopologyGuarantee { - /// Validate only the pseudomanifold / manifold-with-boundary invariants: - /// - facet degree (1 or 2 incident simplices per facet) - /// - closed boundary ("no boundary of boundary") - Pseudomanifold, - - /// Validate PL-manifold invariants (incremental mode). - /// - /// This includes all `Pseudomanifold` checks plus ridge-link validation during - /// insertion, with a required vertex-link validation at construction completion. - PLManifold, - - /// Validate PL-manifold invariants with strict per-insertion checks. - /// - /// This includes all `Pseudomanifold` checks plus vertex-link validation - /// after every insertion (slowest, maximum safety). - PLManifoldStrict, -} - -impl Default for TopologyGuarantee { - #[inline] - fn default() -> Self { - Self::DEFAULT - } -} - -impl TopologyGuarantee { - /// The default topology guarantee used when constructing triangulations. - /// - /// This is a `const` alternative to `::default()` for `const fn` constructors. - pub const DEFAULT: Self = Self::PLManifold; - - /// Returns `true` if this topology guarantee requires vertex-link validation - /// after each insertion. - #[inline] - #[must_use] - pub const fn requires_vertex_links_during_insertion(self) -> bool { - matches!(self, Self::PLManifoldStrict) - } - - /// Returns `true` if this topology guarantee requires vertex-link validation - /// at construction completion. - #[inline] - #[must_use] - pub const fn requires_vertex_links_at_completion(self) -> bool { - matches!(self, Self::PLManifold | Self::PLManifoldStrict) - } - - /// Returns `true` if this topology guarantee requires pseudomanifold checks - /// during insertion. - /// - /// All current guarantees require the codimension-1 facet-degree and - /// codimension-2 closed-boundary conditions. Stronger guarantees layer - /// ridge-link and vertex-link validation on top of these checks. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::TopologyGuarantee; - /// - /// assert!( - /// TopologyGuarantee::Pseudomanifold - /// .requires_pseudomanifold_checks_during_insertion() - /// ); - /// assert!( - /// TopologyGuarantee::PLManifold - /// .requires_pseudomanifold_checks_during_insertion() - /// ); - /// ``` - #[inline] - #[must_use] - pub const fn requires_pseudomanifold_checks_during_insertion(self) -> bool { - matches!( - self, - Self::Pseudomanifold | Self::PLManifold | Self::PLManifoldStrict - ) - } - - /// Returns `true` if this topology guarantee requires ridge-link validation. - /// - /// Ridge-link validation is fast (O(local)) and catches many PL-manifold violations, - /// providing good error detection even with reduced validation frequency. - #[inline] - #[must_use] - pub const fn requires_ridge_links(self) -> bool { - matches!(self, Self::PLManifold | Self::PLManifoldStrict) - } - - /// Returns the [`ValidationPolicy`] that should be used by default for this guarantee. - /// - /// [`PLManifoldStrict`](Self::PLManifoldStrict) uses [`Always`](ValidationPolicy::Always) - /// so that full Level-3 global validation (including vertex-link checks) runs - /// after every insertion — this is the strongest and slowest setting. - /// All other guarantees default to - /// [`OnSuspicion`](ValidationPolicy::OnSuspicion). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::{TopologyGuarantee, ValidationPolicy}; - /// - /// assert_eq!( - /// TopologyGuarantee::PLManifoldStrict.default_validation_policy(), - /// ValidationPolicy::Always, - /// ); - /// assert_eq!( - /// TopologyGuarantee::PLManifold.default_validation_policy(), - /// ValidationPolicy::OnSuspicion, - /// ); - /// ``` - #[inline] - #[must_use] - pub const fn default_validation_policy(self) -> ValidationPolicy { - match self { - Self::PLManifoldStrict => ValidationPolicy::Always, - _ => ValidationPolicy::OnSuspicion, - } - } - - /// Returns `true` if this guarantee is compatible with the given validation policy. - /// - /// `PLManifold` requires at least end-of-construction validation, so it's incompatible - /// with `ValidationPolicy::Never`. - #[inline] - #[must_use] - pub const fn is_compatible_with_policy(self, policy: ValidationPolicy) -> bool { - match self { - Self::Pseudomanifold => true, - Self::PLManifold | Self::PLManifoldStrict => !matches!(policy, ValidationPolicy::Never), - } - } -} +use crate::topology::traits::topological_space::GlobalTopology; /// Generic triangulation combining kernel and data structure. /// @@ -1147,8 +28,7 @@ impl TopologyGuarantee { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::Triangulation; -/// use delaunay::prelude::geometry::FastKernel; +/// use delaunay::prelude::triangulation::{FastKernel, Triangulation}; /// /// let tri: Triangulation, (), (), 3> = /// Triangulation::new_empty(FastKernel::new()); @@ -1167,93 +47,26 @@ pub struct Triangulation, U, V, const D: usize> { } // ============================================================================= -// Internal Helpers (Structural / Graph Traversals) +// Basic Accessors (Minimal Bounds) // ============================================================================= impl Triangulation where K: Kernel, { - /// Traverses the simplex neighbor graph starting at `start` and returns the set of visited simplices. + /// Create an empty triangulation with the given kernel. + /// + /// # Examples /// - /// If `allowed` is `Some`, traversal is restricted to that set. Neighbors outside the allowed - /// set are reported via `on_external_neighbor`. - #[must_use] - fn traverse_simplex_neighbor_graph( - &self, - start: SimplexKey, - reserve: usize, - allowed: Option<&SimplexKeySet>, - mut on_external_neighbor: F, - ) -> SimplexKeySet - where - F: FnMut(SimplexKey, SimplexKey), - { - let mut visited: SimplexKeySet = SimplexKeySet::default(); - visited.reserve(reserve); - - let mut stack: SimplexKeyBuffer = SimplexKeyBuffer::new(); - stack.push(start); - - while let Some(ck) = stack.pop() { - if !visited.insert(ck) { - continue; - } - - let Some(simplex) = self.tds.simplex(ck) else { - continue; - }; - - let Some(neighbors) = simplex.neighbor_keys() else { - continue; - }; - - for n_opt in neighbors { - let Some(nk) = n_opt else { - continue; - }; - - if !self.tds.contains_simplex(nk) { - continue; - } - - if allowed.is_some_and(|allowed| !allowed.contains(&nk)) { - on_external_neighbor(ck, nk); - continue; - } - - if !visited.contains(&nk) { - stack.push(nk); - } - } - } - - visited - } -} - -// ============================================================================= -// Basic Accessors (Minimal Bounds) -// ============================================================================= - -impl Triangulation -where - K: Kernel, -{ - /// Create an empty triangulation with the given kernel. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::*; - /// use delaunay::prelude::triangulation::*; - /// - /// let tri: Triangulation, (), (), 3> = - /// Triangulation::new_empty(FastKernel::new()); - /// assert_eq!(tri.number_of_vertices(), 0); - /// assert_eq!(tri.number_of_simplices(), 0); - /// assert_eq!(tri.dim(), -1); // Empty triangulation has dimension -1 - /// ``` + /// ```rust + /// use delaunay::prelude::triangulation::{FastKernel, Triangulation}; + /// + /// let tri: Triangulation, (), (), 3> = + /// Triangulation::new_empty(FastKernel::new()); + /// assert_eq!(tri.number_of_vertices(), 0); + /// assert_eq!(tri.number_of_simplices(), 0); + /// assert_eq!(tri.dim(), -1); // Empty triangulation has dimension -1 + /// ``` #[must_use] pub fn new_empty(kernel: K) -> Self { Self { @@ -1265,200 +78,6 @@ where } } - /// Returns the topology guarantee used for Level 3 topology validation. - #[inline] - #[must_use] - pub const fn topology_guarantee(&self) -> TopologyGuarantee { - self.topology_guarantee - } - - /// Returns the runtime global topology metadata associated with this triangulation. - #[inline] - #[must_use] - pub const fn global_topology(&self) -> GlobalTopology { - self.global_topology - } - - /// Returns the high-level topology kind (`Euclidean`, `Toroidal`, etc.). - #[inline] - #[must_use] - pub const fn topology_kind(&self) -> TopologyKind { - self.global_topology.kind() - } - - /// Sets runtime global topology metadata on the triangulation. - #[inline] - pub const fn set_global_topology(&mut self, global_topology: GlobalTopology) { - self.global_topology = global_topology; - } - - /// Returns the insertion-time global topology validation policy used by the triangulation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::{Triangulation, ValidationPolicy}; - /// use delaunay::prelude::geometry::FastKernel; - /// - /// let tri: Triangulation, (), (), 2> = - /// Triangulation::new_empty(FastKernel::new()); - /// - /// assert_eq!(tri.validation_policy(), ValidationPolicy::OnSuspicion); - /// ``` - #[inline] - #[must_use] - pub const fn validation_policy(&self) -> ValidationPolicy { - self.validation_policy - } - - /// Sets the insertion-time global topology validation policy used by the triangulation. - /// - /// If the requested policy is incompatible with the current topology guarantee (for example, - /// `ValidationPolicy::Never` with `TopologyGuarantee::PLManifold`), this runs - /// [`Triangulation::validate_at_completion`](Self::validate_at_completion) to provide - /// immediate feedback and emits a warning. Call `validate_at_completion()` after batch - /// construction when using an incompatible combination. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::{Triangulation, ValidationPolicy}; - /// use delaunay::prelude::geometry::FastKernel; - /// - /// let mut tri: Triangulation, (), (), 2> = - /// Triangulation::new_empty(FastKernel::new()); - /// - /// tri.set_validation_policy(ValidationPolicy::Always); - /// assert_eq!(tri.validation_policy(), ValidationPolicy::Always); - /// ``` - #[inline] - pub fn set_validation_policy(&mut self, policy: ValidationPolicy) { - if !self.topology_guarantee.is_compatible_with_policy(policy) { - let completion_result = self.validate_at_completion(); - - if let Err(err) = completion_result { - debug_assert!( - false, - "Validation policy {policy:?} is incompatible with topology guarantee {guarantee:?}; validate_at_completion failed: {err}", - guarantee = self.topology_guarantee - ); - tracing::warn!( - "Validation policy {policy:?} is incompatible with topology guarantee {guarantee:?}; validate_at_completion failed: {err}. Validation policy not updated.", - guarantee = self.topology_guarantee - ); - return; - } - - tracing::warn!( - "Validation policy {policy:?} is incompatible with topology guarantee {guarantee:?}; call validate_at_completion() after construction to certify PL-manifoldness.", - guarantee = self.topology_guarantee - ); - } - - self.validation_policy = policy; - } - - /// Sets the topology guarantee used for Level 3 topology validation. - /// - /// If the requested guarantee is incompatible with the current validation policy (for - /// example, `ValidationPolicy::Never` with `TopologyGuarantee::PLManifold`), this runs - /// [`Triangulation::validate_at_completion`](Self::validate_at_completion) to provide - /// immediate feedback and emits a warning. Call `validate_at_completion()` after batch - /// construction when using an incompatible combination. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::{TopologyGuarantee, Triangulation}; - /// use delaunay::prelude::geometry::FastKernel; - /// - /// let mut tri: Triangulation, (), (), 2> = - /// Triangulation::new_empty(FastKernel::new()); - /// tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - /// assert_eq!(tri.topology_guarantee(), TopologyGuarantee::Pseudomanifold); - /// ``` - #[inline] - pub fn set_topology_guarantee(&mut self, guarantee: TopologyGuarantee) { - if !guarantee.is_compatible_with_policy(self.validation_policy) { - let previous = self.topology_guarantee; - self.topology_guarantee = guarantee; - let completion_result = self.validate_at_completion(); - - if let Err(err) = completion_result { - self.topology_guarantee = previous; - debug_assert!( - false, - "Topology guarantee {guarantee:?} is incompatible with validation policy {policy:?}; validate_at_completion failed: {err}", - policy = self.validation_policy - ); - tracing::warn!( - "Topology guarantee {guarantee:?} is incompatible with validation policy {policy:?}; validate_at_completion failed: {err}. Topology guarantee not updated.", - policy = self.validation_policy - ); - return; - } - - self.topology_guarantee = previous; - tracing::warn!( - "Topology guarantee {guarantee:?} is incompatible with validation policy {policy:?}; call validate_at_completion() after construction to certify PL-manifoldness.", - policy = self.validation_policy - ); - } - - self.topology_guarantee = guarantee; - } - - /// Returns the number of times the topology safety-net recovered from a Level 3 - /// validation failure by retrying insertion with a star-split of the containing simplex. - /// - /// This is a process-wide counter (across all triangulation instances) intended for - /// production telemetry. A high value suggests the cavity-based insertion frequently - /// creates transient invalid topology that is being masked by the fallback. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::FastKernel; - /// use delaunay::prelude::triangulation::Triangulation; - /// - /// let count = Triangulation::, (), (), 3> - /// ::topology_safety_net_star_split_fallback_successes(); - /// assert!(count >= 0); - /// ``` - #[must_use] - pub fn topology_safety_net_star_split_fallback_successes() -> u64 { - TOPOLOGY_SAFETY_NET_STAR_SPLIT_FALLBACK_SUCCESSES.load(Ordering::Relaxed) - } - - /// Returns duplicate-detection telemetry if enabled via `DELAUNAY_DUPLICATE_METRICS`. - /// - /// This is a process-wide counter (across all triangulation instances). It reports how often - /// duplicate checks used the hash grid versus falling back to linear scans, along with the - /// total candidate count inspected during grid queries. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::FastKernel; - /// use delaunay::prelude::triangulation::{DuplicateDetectionMetrics, Triangulation}; - /// - /// let metrics = Triangulation::, (), (), 3> - /// ::duplicate_detection_metrics(); - /// let _ = metrics; // None unless DELAUNAY_DUPLICATE_METRICS is set - /// ``` - #[must_use] - pub fn duplicate_detection_metrics() -> Option { - if !duplicate_detection_metrics_enabled() { - return None; - } - Some(DuplicateDetectionMetrics { - total_checks: DUPLICATE_DETECTION_TOTAL.load(Ordering::Relaxed), - grid_used: DUPLICATE_DETECTION_GRID_USED.load(Ordering::Relaxed), - grid_fallbacks: DUPLICATE_DETECTION_GRID_FALLBACKS.load(Ordering::Relaxed), - grid_candidates: DUPLICATE_DETECTION_GRID_CANDIDATES.load(Ordering::Relaxed), - }) - } - #[cfg(test)] #[inline] pub(crate) const fn new_with_tds(kernel: K, tds: Tds) -> Self { @@ -1471,60 +90,6 @@ where } } - /// Returns an iterator over all simplices in the triangulation. - /// - /// Delegates to the underlying Tds. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// // Iterate over simplices - /// for (_simplex_key, simplex) in tri.simplices() { - /// assert_eq!(simplex.number_of_vertices(), 3); // 2D triangle - /// } - /// assert_eq!(tri.simplices().count(), 1); - /// ``` - pub fn simplices(&self) -> impl Iterator)> { - self.tds.simplices() - } - - /// Returns an iterator over all vertices in the triangulation. - /// - /// Delegates to the underlying Tds. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// // Iterate over vertices - /// for (_vertex_key, vertex) in tri.vertices() { - /// assert_eq!(vertex.dim(), 2); // 2D vertices - /// } - /// assert_eq!(tri.vertices().count(), 3); - /// ``` - pub fn vertices(&self) -> impl Iterator)> { - self.tds.vertices() - } - /// Sets the auxiliary data on a vertex, returning the previous value. /// /// Delegates to [`Tds::set_vertex_data`]. This is a safe O(1) operation @@ -1538,7 +103,9 @@ where /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulationBuilder, Vertex, vertex, + /// }; /// /// let vertices: [Vertex; 3] = [ /// vertex!([0.0, 0.0], 10i32), @@ -1575,7 +142,9 @@ where /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulationBuilder, vertex, + /// }; /// /// let vertices = [ /// vertex!([0.0, 0.0]), @@ -1598,11164 +167,46 @@ where pub fn set_simplex_data(&mut self, key: SimplexKey, data: Option) -> Option> { self.tds.set_simplex_data(key, data) } +} - /// Returns the number of vertices in the triangulation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// 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::new(&vertices).unwrap(); - /// assert_eq!(dt.as_triangulation().number_of_vertices(), 4); - /// ``` - #[must_use] - pub fn number_of_vertices(&self) -> usize { - self.tds.number_of_vertices() - } - - /// Returns the number of simplices in the triangulation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// 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::new(&vertices).unwrap(); - /// assert_eq!(dt.as_triangulation().number_of_simplices(), 1); // Single tetrahedron - /// ``` - #[must_use] - pub fn number_of_simplices(&self) -> usize { - self.tds.number_of_simplices() - } - - /// Returns the dimension of the triangulation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::*; - /// use delaunay::prelude::triangulation::*; - /// - /// // 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![ - /// 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::new(&vertices).unwrap(); - /// assert_eq!(dt.as_triangulation().dim(), 3); - /// ``` - #[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 facet is represented as a lightweight `FacetView` that references the - /// underlying triangulation data. - /// - /// # Returns - /// - /// An iterator yielding `FacetView` objects for all facets in the triangulation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// 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::new(&vertices).unwrap(); - /// - /// // Iterate over all facets - /// let facet_count = dt.as_triangulation().facets().count(); - /// assert_eq!(facet_count, 4); // Tetrahedron has 4 facets - /// ``` - pub fn facets(&self) -> AllFacetsIter<'_, K::Scalar, U, V, D> { - AllFacetsIter::new(&self.tds) - } - - /// Returns an iterator over boundary (hull) facets in the triangulation. - /// - /// Boundary facets are those that belong to exactly one simplex. This method - /// computes the facet-to-simplices map internally for convenience. - /// - /// # Returns - /// - /// An iterator yielding `FacetView` objects for boundary facets only. - /// - /// # Panics - /// - /// Panics if the triangulation data structure is corrupted (simplices have invalid - /// neighbor relationships or facet information). This indicates a bug in the - /// library and should never happen with a properly constructed triangulation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// 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::new(&vertices).unwrap(); - /// - /// let boundary_count = dt.as_triangulation().boundary_facets().count(); - /// assert_eq!(boundary_count, 4); // All facets are on boundary - /// ``` - pub fn boundary_facets(&self) -> BoundaryFacetsIter<'_, K::Scalar, U, V, D> { - // build_facet_to_simplices_map only fails if simplices have invalid structure, - // which should never happen in a valid triangulation - let facet_map = self - .tds - .build_facet_to_simplices_map() - .expect("Failed to build facet map - triangulation structure is corrupted"); - BoundaryFacetsIter::new(&self.tds, facet_map) - } +#[cfg(test)] +mod tests { + use super::*; + use crate::geometry::kernel::FastKernel; + use slotmap::KeyData; - // ============================================================================= - // Public Topology Traversal & Adjacency API (Read-only) - // ============================================================================= + #[test] + fn new_empty_sets_default_topology_and_validation_policy() { + let tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); - #[inline] - fn debug_assert_adjacency_index_matches(&self, index: &AdjacencyIndex) { - // AdjacencyIndex is built from a snapshot of a triangulation. We cannot enforce at - // compile-time that an index belongs to this triangulation, but we can cheaply catch - // obvious mix-ups in debug builds. - debug_assert_eq!( - index.vertex_to_simplices.len(), - self.tds.number_of_vertices(), - "AdjacencyIndex vertex_to_simplices size does not match triangulation vertex count" - ); - debug_assert_eq!( - index.vertex_to_edges.len(), - self.tds.number_of_vertices(), - "AdjacencyIndex vertex_to_edges size does not match triangulation vertex count" + assert_eq!(tri.tds.number_of_vertices(), 0); + assert_eq!(tri.tds.number_of_simplices(), 0); + assert_eq!(tri.global_topology, GlobalTopology::DEFAULT); + assert_eq!(tri.topology_guarantee, TopologyGuarantee::DEFAULT); + assert_eq!( + tri.validation_policy, + TopologyGuarantee::DEFAULT.default_validation_policy() ); } - /// Returns an iterator over all unique edges in the triangulation. - /// - /// Edges are inferred from the vertex lists of each simplex; they are not stored explicitly. - /// - /// ## Allocation and iteration order - /// - /// This method allocates an internal set to deduplicate edges. The iteration order is - /// not specified. - /// - /// If you need fast repeated topology queries, consider building an - /// [`AdjacencyIndex`] once via [`Triangulation::build_adjacency_index`](Self::build_adjacency_index). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// // A single 3D tetrahedron has 6 unique edges. - /// 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::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// let edges: std::collections::HashSet<_> = tri.edges().collect(); - /// assert_eq!(edges.len(), 6); - /// ``` - pub fn edges(&self) -> impl Iterator + '_ { - self.collect_edges().into_iter() - } - - /// Returns an iterator over all unique edges using a precomputed [`AdjacencyIndex`]. - /// - /// This avoids per-call deduplication and allocations. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// // A single 3D tetrahedron has 6 unique edges. - /// 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::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// let index = tri.build_adjacency_index().unwrap(); - /// let edges: std::collections::HashSet<_> = tri.edges_with_index(&index).collect(); - /// assert_eq!(edges.len(), 6); - /// ``` - pub fn edges_with_index<'a>( - &self, - index: &'a AdjacencyIndex, - ) -> impl Iterator + 'a { - self.debug_assert_adjacency_index_matches(index); - index.edges() - } + #[test] + fn set_vertex_data_returns_none_for_invalid_key() { + let mut tri: Triangulation, i32, (), 2> = + Triangulation::new_empty(FastKernel::new()); + let stale = VertexKey::from(KeyData::from_ffi(0xDEAD_BEEF)); - /// Returns the number of unique edges in the triangulation. - /// - /// This is equivalent to `self.edges().count()`. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// // A single 2D triangle has 3 unique edges. - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// assert_eq!(tri.number_of_edges(), 3); - /// ``` - #[must_use] - pub fn number_of_edges(&self) -> usize { - self.collect_edges().len() + assert_eq!(tri.set_vertex_data(stale, Some(42)), None); + assert_eq!(tri.tds.number_of_vertices(), 0); } - /// Returns the number of unique edges using a precomputed [`AdjacencyIndex`]. - /// - /// This is equivalent to `self.edges_with_index(index).count()`. - /// - /// # Examples - /// - /// ```rust - /// # use delaunay::prelude::query::*; - /// # 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::new(&vertices).unwrap(); - /// # let tri = dt.as_triangulation(); - /// # let index = tri.build_adjacency_index().unwrap(); - /// assert_eq!(tri.number_of_edges_with_index(&index), 6); - /// ``` - #[must_use] - pub fn number_of_edges_with_index(&self, index: &AdjacencyIndex) -> usize { - self.debug_assert_adjacency_index_matches(index); - index.number_of_edges() - } - - /// Returns an iterator over all simplices adjacent (incident) to a vertex. - /// - /// If `v` is not present in this triangulation, the iterator is empty. - /// - /// Iteration order is not specified. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// // Two tetrahedra sharing a triangular facet. - /// let vertices: Vec<_> = vec![ - /// // Shared triangle - /// vertex!([0.0, 0.0, 0.0]), - /// vertex!([2.0, 0.0, 0.0]), - /// vertex!([1.0, 2.0, 0.0]), - /// // Two apices - /// vertex!([1.0, 0.7, 1.5]), - /// vertex!([1.0, 0.7, -1.5]), - /// ]; - /// - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// // Find a vertex on the shared triangle by coordinates. - /// let shared_vertex_key = tri - /// .vertices() - /// .find_map(|(vk, _)| { - /// let coords = tri.vertex_coords(vk)?; - /// (coords == [0.0, 0.0, 0.0]).then_some(vk) - /// }) - /// .unwrap(); - /// - /// // The shared vertex is incident to both simplices. - /// assert_eq!(tri.adjacent_simplices(shared_vertex_key).count(), 2); - /// ``` - pub fn adjacent_simplices(&self, v: VertexKey) -> impl Iterator + '_ { - self.tds - .find_simplices_containing_vertex_by_key(v) - .into_iter() - } - - /// Returns an iterator over all simplices adjacent (incident) to a vertex using a precomputed - /// [`AdjacencyIndex`]. - /// - /// This avoids per-call scans of the triangulation. - /// - /// If `v` is not present in the index, the iterator is empty. - /// - /// # Examples - /// - /// ```rust - /// # use delaunay::prelude::query::*; - /// # let vertices: Vec<_> = vec![ - /// # // Shared triangle - /// # vertex!([0.0, 0.0, 0.0]), - /// # vertex!([2.0, 0.0, 0.0]), - /// # vertex!([1.0, 2.0, 0.0]), - /// # // Two apices - /// # vertex!([1.0, 0.7, 1.5]), - /// # vertex!([1.0, 0.7, -1.5]), - /// # ]; - /// # let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); - /// # let tri = dt.as_triangulation(); - /// # let index = tri.build_adjacency_index().unwrap(); - /// let v = tri.vertices().next().unwrap().0; - /// assert!(tri.adjacent_simplices_with_index(&index, v).count() >= 1); - /// ``` - pub fn adjacent_simplices_with_index<'a>( - &self, - index: &'a AdjacencyIndex, - v: VertexKey, - ) -> impl Iterator + 'a { - self.debug_assert_adjacency_index_matches(index); - index.adjacent_simplices(v) - } - - /// Returns the number of simplices adjacent (incident) to a vertex using a precomputed - /// [`AdjacencyIndex`]. - /// - /// If `v` is not present in the index, returns 0. - /// - /// # Examples - /// - /// ```rust - /// # use delaunay::prelude::query::*; - /// # 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::new(&vertices).unwrap(); - /// # let tri = dt.as_triangulation(); - /// # let index = tri.build_adjacency_index().unwrap(); - /// let v0 = tri.vertices().next().unwrap().0; - /// assert_eq!(tri.number_of_adjacent_simplices_with_index(&index, v0), 1); - /// ``` - #[must_use] - pub fn number_of_adjacent_simplices_with_index( - &self, - index: &AdjacencyIndex, - v: VertexKey, - ) -> usize { - self.debug_assert_adjacency_index_matches(index); - index.number_of_adjacent_simplices(v) - } - - /// Returns an iterator over all neighbors of a simplex. - /// - /// Boundary facets are omitted (only existing neighbors are yielded). If `c` is not - /// present, the iterator is empty. - /// - /// Iteration order is not specified. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// // Two tetrahedra sharing a triangular facet => each tetra has exactly one neighbor. - /// let vertices: Vec<_> = vec![ - /// // Shared triangle - /// vertex!([0.0, 0.0, 0.0]), - /// vertex!([2.0, 0.0, 0.0]), - /// vertex!([1.0, 2.0, 0.0]), - /// // Two apices - /// vertex!([1.0, 0.7, 1.5]), - /// vertex!([1.0, 0.7, -1.5]), - /// ]; - /// - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// let simplex_keys: Vec<_> = tri.simplices().map(|(ck, _)| ck).collect(); - /// assert_eq!(simplex_keys.len(), 2); - /// - /// for &ck in &simplex_keys { - /// let neighbors: Vec<_> = tri.simplex_neighbors(ck).collect(); - /// assert_eq!(neighbors.len(), 1); - /// assert!(simplex_keys.contains(&neighbors[0])); - /// assert_ne!(neighbors[0], ck); - /// } - /// ``` - pub fn simplex_neighbors(&self, c: SimplexKey) -> impl Iterator + '_ { - self.tds - .simplex(c) - .and_then(Simplex::neighbors) - .into_iter() - .flat_map(IntoIterator::into_iter) - .flatten() - .filter(|&neighbor_key| self.tds.contains_simplex(neighbor_key)) - } - - /// Returns an iterator over all neighbors of a simplex using a precomputed [`AdjacencyIndex`]. - /// - /// If `c` is not present in the index, the iterator is empty. - /// - /// # Examples - /// - /// ```rust - /// # use delaunay::prelude::query::*; - /// # let vertices: Vec<_> = vec![ - /// # // Shared triangle - /// # vertex!([0.0, 0.0, 0.0]), - /// # vertex!([2.0, 0.0, 0.0]), - /// # vertex!([1.0, 2.0, 0.0]), - /// # // Two apices - /// # vertex!([1.0, 0.7, 1.5]), - /// # vertex!([1.0, 0.7, -1.5]), - /// # ]; - /// # let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); - /// # let tri = dt.as_triangulation(); - /// # let index = tri.build_adjacency_index().unwrap(); - /// let simplex_key = tri.simplices().next().unwrap().0; - /// let neighbors: Vec<_> = tri.simplex_neighbors_with_index(&index, simplex_key).collect(); - /// assert_eq!(neighbors.len(), 1); - /// ``` - pub fn simplex_neighbors_with_index<'a>( - &self, - index: &'a AdjacencyIndex, - c: SimplexKey, - ) -> impl Iterator + 'a { - self.debug_assert_adjacency_index_matches(index); - index.simplex_neighbors(c) - } - - /// Returns the number of neighbors of a simplex using a precomputed [`AdjacencyIndex`]. - /// - /// If `c` is not present in the index, returns 0. - /// - /// # Examples - /// - /// ```rust - /// # use delaunay::prelude::query::*; - /// # let vertices: Vec<_> = vec![ - /// # // Shared triangle - /// # vertex!([0.0, 0.0, 0.0]), - /// # vertex!([2.0, 0.0, 0.0]), - /// # vertex!([1.0, 2.0, 0.0]), - /// # // Two apices - /// # vertex!([1.0, 0.7, 1.5]), - /// # vertex!([1.0, 0.7, -1.5]), - /// # ]; - /// # let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); - /// # let tri = dt.as_triangulation(); - /// # let index = tri.build_adjacency_index().unwrap(); - /// let simplex_key = tri.simplices().next().unwrap().0; - /// assert_eq!(tri.number_of_simplex_neighbors_with_index(&index, simplex_key), 1); - /// ``` - #[must_use] - pub fn number_of_simplex_neighbors_with_index( - &self, - index: &AdjacencyIndex, - c: SimplexKey, - ) -> usize { - self.debug_assert_adjacency_index_matches(index); - index.number_of_simplex_neighbors(c) - } - - /// Returns an iterator over all unique edges incident to a vertex. - /// - /// If `v` is not present in this triangulation, the iterator is empty. - /// - /// ## Allocation and iteration order - /// - /// This method allocates an internal set to deduplicate edges. The iteration order is - /// not specified. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// // In a single tetrahedron, each vertex has degree 3. - /// 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::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// let v0 = tri.vertices().next().unwrap().0; - /// let incident: Vec<_> = tri.incident_edges(v0).collect(); - /// assert_eq!(incident.len(), 3); - /// assert!(incident - /// .iter() - /// .all(|e| matches!(e.endpoints(), (a, b) if a == v0 || b == v0))); - /// ``` - pub fn incident_edges(&self, v: VertexKey) -> impl Iterator + '_ { - self.collect_incident_edges(v).into_iter() - } - - /// Returns an iterator over all unique edges incident to a vertex using a precomputed - /// [`AdjacencyIndex`]. - /// - /// If `v` is not present in the index, the iterator is empty. - /// - /// # Examples - /// - /// ```rust - /// # use delaunay::prelude::query::*; - /// # 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::new(&vertices).unwrap(); - /// # let tri = dt.as_triangulation(); - /// # let index = tri.build_adjacency_index().unwrap(); - /// let v0 = tri.vertices().next().unwrap().0; - /// assert_eq!(tri.incident_edges_with_index(&index, v0).count(), 3); - /// ``` - pub fn incident_edges_with_index<'a>( - &self, - index: &'a AdjacencyIndex, - v: VertexKey, - ) -> impl Iterator + 'a { - self.debug_assert_adjacency_index_matches(index); - index.incident_edges(v) - } - - /// Returns the number of unique edges incident to a vertex using a precomputed - /// [`AdjacencyIndex`]. - /// - /// If `v` is not present in the index, returns 0. - /// - /// # Examples - /// - /// ```rust - /// # use delaunay::prelude::query::*; - /// # 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::new(&vertices).unwrap(); - /// # let tri = dt.as_triangulation(); - /// # let index = tri.build_adjacency_index().unwrap(); - /// let v0 = tri.vertices().next().unwrap().0; - /// assert_eq!(tri.number_of_incident_edges_with_index(&index, v0), 3); - /// ``` - #[must_use] - pub fn number_of_incident_edges_with_index( - &self, - index: &AdjacencyIndex, - v: VertexKey, - ) -> usize { - self.debug_assert_adjacency_index_matches(index); - index.number_of_incident_edges(v) - } - - /// Returns the number of unique edges incident to a vertex. - /// - /// If `v` is not present in this triangulation, returns 0. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// // In a single tetrahedron, each vertex has degree 3. - /// 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::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// let v0 = tri.vertices().next().unwrap().0; - /// assert_eq!(tri.number_of_incident_edges(v0), 3); - /// ``` - #[must_use] - pub fn number_of_incident_edges(&self, v: VertexKey) -> usize { - self.collect_incident_edges(v).len() - } - - /// Returns a slice view of a simplex's vertex keys. - /// - /// This is a zero-allocation accessor. If `c` is not present, returns `None`. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// let simplex_key = tri.simplices().next().unwrap().0; - /// let simplex_vertices = tri.simplex_vertices(simplex_key).unwrap(); - /// assert_eq!(simplex_vertices.len(), 3); // D+1 for a 2D simplex - /// ``` - #[must_use] - pub fn simplex_vertices(&self, c: SimplexKey) -> Option<&[VertexKey]> { - self.tds.simplex(c).map(Simplex::vertices) - } - - /// Returns a slice view of a vertex's coordinates. - /// - /// This is a zero-allocation accessor. If `v` is not present, returns `None`. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// // Find the key for a known vertex by matching coordinates. - /// let v_key = tri - /// .vertices() - /// .find_map(|(vk, _)| (tri.vertex_coords(vk)? == [1.0, 0.0]).then_some(vk)) - /// .unwrap(); - /// - /// assert_eq!(tri.vertex_coords(v_key).unwrap(), [1.0, 0.0]); - /// ``` - #[must_use] - pub fn vertex_coords(&self, v: VertexKey) -> Option<&[K::Scalar]> { - self.tds - .vertex(v) - .map(|vertex| &vertex.point().coords()[..]) - } - - /// Builds an immutable adjacency index for fast repeated topology queries. - /// - /// This never stores any cache internally and does not mutate the triangulation. - /// - /// ## Notes - /// - /// - No sorted-order guarantees are provided for the values. - /// - The returned collections are optimized for performance. - /// - The maps include an entry for every vertex currently stored in the triangulation. - /// During the bootstrap phase (before the initial simplex is created), vertices have empty - /// adjacency lists because no simplices exist yet. This is expected and not an error condition. - /// - Isolated vertices (present in the vertex store but not referenced by any simplex) are allowed at - /// the TDS structural layer, but violate the Level 3 manifold invariants checked by - /// [`Triangulation::is_valid`](Self::is_valid). When present, their adjacency lists are empty. - /// - /// # Errors - /// - /// Returns an error if the triangulation data structure is internally inconsistent - /// (e.g., a simplex references a missing vertex key or a missing neighbor simplex key). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// // Two tetrahedra sharing a triangular facet. - /// let vertices: Vec<_> = vec![ - /// // Shared triangle - /// vertex!([0.0, 0.0, 0.0]), - /// vertex!([2.0, 0.0, 0.0]), - /// vertex!([1.0, 2.0, 0.0]), - /// // Two apices - /// vertex!([1.0, 0.7, 1.5]), - /// vertex!([1.0, 0.7, -1.5]), - /// ]; - /// - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); - /// let tri = dt.as_triangulation(); - /// - /// let index = tri.build_adjacency_index().unwrap(); - /// - /// // The index exposes adjacency maps keyed by VertexKey / SimplexKey. - /// let simplex_keys: Vec<_> = tri.simplices().map(|(ck, _)| ck).collect(); - /// for &ck in &simplex_keys { - /// let neighbors = index.simplex_to_neighbors.get(&ck).unwrap(); - /// assert_eq!(neighbors.len(), 1); - /// } - /// ``` - pub fn build_adjacency_index(&self) -> Result { - let vertex_cap = self.tds.number_of_vertices(); - let simplex_cap = self.tds.number_of_simplices(); - - let mut vertex_to_simplices: VertexToSimplicesMap = fast_hash_map_with_capacity(vertex_cap); - let mut simplex_to_neighbors: FastHashMap< - SimplexKey, - SmallBuffer, - > = fast_hash_map_with_capacity(simplex_cap); - let mut vertex_to_edges: FastHashMap< - VertexKey, - SmallBuffer, - > = fast_hash_map_with_capacity(vertex_cap); - - // Deduplicate edges globally while building the index. - let edges_per_simplex = (D + 1).saturating_mul(D) / 2; - let mut seen_edges: FastHashSet = - fast_hash_set_with_capacity(simplex_cap.saturating_mul(edges_per_simplex)); - - for (simplex_key, simplex) in self.tds.simplices() { - let vertices = simplex.vertices(); - - // Vertex → simplices - for &vk in vertices { - if !self.tds.contains_vertex_key(vk) { - return Err(AdjacencyIndexBuildError::MissingVertexKey { - simplex_key, - vertex_key: vk, - }); - } - let entry = vertex_to_simplices.entry(vk).or_default(); - #[cfg(debug_assertions)] - let was_spilled = entry.spilled(); - entry.push(simplex_key); - #[cfg(debug_assertions)] - if !was_spilled && entry.spilled() { - let spill_count = - VERTEX_TO_SIMPLICES_SPILL_EVENTS.fetch_add(1, Ordering::Relaxed) + 1; - tracing::debug!( - "VertexToSimplicesMap spill #{spill_count}: vertex={vk:?} len={} cap={} (MAX_PRACTICAL_DIMENSION_SIZE={MAX_PRACTICAL_DIMENSION_SIZE})", - entry.len(), - entry.capacity() - ); - } - } - - // Simplex → neighbors - if let Some(neighbors) = simplex.neighbor_keys() { - let mut neighs: SmallBuffer = - SmallBuffer::new(); - - for n_opt in neighbors { - let Some(nk) = n_opt else { - continue; - }; - - if !self.tds.contains_simplex(nk) { - return Err(AdjacencyIndexBuildError::MissingNeighborSimplex { - simplex_key, - neighbor_key: nk, - }); - } - - neighs.push(nk); - } - - if !neighs.is_empty() { - simplex_to_neighbors.insert(simplex_key, neighs); - } - } - - // Vertex → edges (deduped) - for i in 0..vertices.len() { - for j in (i + 1)..vertices.len() { - let edge = EdgeKey::new(vertices[i], vertices[j]); - if !seen_edges.insert(edge) { - continue; - } - - let (a, b) = edge.endpoints(); - vertex_to_edges.entry(a).or_default().push(edge); - vertex_to_edges.entry(b).or_default().push(edge); - } - } - } - - // Ensure every vertex in the triangulation has an entry, even if it is currently - // not incident to any simplex (e.g., bootstrap phase with < D+1 vertices, or TDS-level - // states with isolated vertices). Level 3 topology validation (`Triangulation::is_valid`) - // rejects isolated vertices, but this indexing helper remains usable for debugging and - // intermediate construction states. - for (vk, _) in self.tds.vertices() { - vertex_to_simplices.entry(vk).or_default(); - vertex_to_edges.entry(vk).or_default(); - } - - Ok(AdjacencyIndex { - vertex_to_edges, - vertex_to_simplices, - simplex_to_neighbors, - }) - } - - #[must_use] - fn collect_edges(&self) -> FastHashSet { - let simplex_cap = self.tds.number_of_simplices(); - let edges_per_simplex = (D + 1).saturating_mul(D) / 2; - - let mut edges: FastHashSet = - fast_hash_set_with_capacity(simplex_cap.saturating_mul(edges_per_simplex)); - - for (_simplex_key, simplex) in self.tds.simplices() { - let vertices = simplex.vertices(); - for i in 0..vertices.len() { - for j in (i + 1)..vertices.len() { - edges.insert(EdgeKey::new(vertices[i], vertices[j])); - } - } - } - - edges - } - - #[must_use] - fn collect_incident_edges(&self, v: VertexKey) -> FastHashSet { - let mut edges: FastHashSet = FastHashSet::default(); - - for simplex_key in self.adjacent_simplices(v) { - let Some(simplex) = self.tds.simplex(simplex_key) else { - continue; - }; - - for &other in simplex.vertices() { - if other == v { - continue; - } - edges.insert(EdgeKey::new(v, other)); - } - } - - edges - } - - /// Collect simplex points for orientation evaluation. - /// - /// For periodic simplices, this delegates per-vertex lattice-offset lifting to the active - /// [`GlobalTopology`] behavior model. - fn collect_simplex_points_for_orientation( - &self, - simplex_key: SimplexKey, - simplex: &Simplex, - purpose: &str, - ) -> Result, MAX_PRACTICAL_DIMENSION_SIZE>, TdsError> { - let topology_model = self.global_topology.model(); - let periodic_offsets = simplex.periodic_vertex_offsets(); - if let Some(offsets) = periodic_offsets { - // Check length invariant - if offsets.len() != simplex.number_of_vertices() { - return Err(TdsError::DimensionMismatch { - expected: simplex.number_of_vertices(), - actual: offsets.len(), - context: format!( - "simplex {:?} (key {simplex_key:?}) periodic offset count vs vertex count during {purpose}", - simplex.uuid(), - ), - }); - } - // Check topology capabilities - if !topology_model.supports_periodic_orientation_offsets() { - return Err(TdsError::InconsistentDataStructure { - message: format!( - "Simplex {:?} (key {simplex_key:?}) has periodic offsets (count {}) during {purpose}, but triangulation global topology is {:?} (kind {:?}, allows_boundary: {}, periodic_domain: {:?}); expected periodic-orientation-offset-capable topology", - simplex.uuid(), - offsets.len(), - self.global_topology, - topology_model.kind(), - topology_model.allows_boundary(), - topology_model.periodic_domain(), - ), - }); - } - } - - let mut points: SmallBuffer, MAX_PRACTICAL_DIMENSION_SIZE> = - SmallBuffer::with_capacity(simplex.number_of_vertices()); - - for (vertex_idx, &vertex_key) in simplex.vertices().iter().enumerate() { - let vertex = self.tds.vertex(vertex_key).ok_or_else(|| { - TdsError::VertexNotFound { - vertex_key, - context: format!( - "referenced by simplex {:?} (key {simplex_key:?}) at position {vertex_idx} during {purpose}", - simplex.uuid(), - ), - } - })?; - let periodic_offset = periodic_offsets.map(|offsets| offsets[vertex_idx]); - let lifted_coords = topology_model - .lift_for_orientation(*vertex.point().coords(), periodic_offset) - .map_err(|error| TdsError::InconsistentDataStructure { - message: format!( - "Failed to lift coordinates for vertex key {vertex_key:?} at slot {vertex_idx} in simplex {:?} (key {simplex_key:?}) during {purpose}: {error}", - simplex.uuid(), - ), - })?; - - points.push(Point::new(lifted_coords)); - } - - Ok(points) - } - - /// Evaluate a simplex's geometric orientation for a specific validation/canonicalization context. - /// - /// This helper centralizes: - /// - lifted-point collection, and - /// - exact (non-SoS) orientation determination via [`robust_orientation`]. - /// - /// # Exact Orientation (no `SoS`) - /// - /// This function uses [`robust_orientation`] exclusively to determine the sign. - /// It does **not** consult the kernel (which may use `SoS`), because: - /// - Callers need the true geometric sign for orientation canonicalization - /// and validation — `SoS` tie-breaking would mask real degeneracies. - /// - Using `robust_orientation` alone avoids duplicate exact-arithmetic work - /// (both `robust_orientation` and `AdaptiveKernel::orientation()` run the - /// same fast-filter + Bareiss pipeline internally). - /// - /// Returns `0` for truly degenerate (zero-volume) simplices, so callers' existing - /// `orientation == 0` handling works correctly. - /// - /// # Error Mapping - /// - /// Conversion failures (e.g. non-finite coordinates) are mapped to - /// [`TdsError::InconsistentDataStructure`] because they indicate - /// an internal problem rather than a geometry degeneracy. - fn evaluate_simplex_orientation_for_context( - &self, - simplex_key: SimplexKey, - simplex: &Simplex, - purpose: &str, - predicate_failure_prefix: &str, - ) -> Result { - let points = self.collect_simplex_points_for_orientation(simplex_key, simplex, purpose)?; - - // Use exact orientation only (no SoS): - // sign to correctly classify degenerate simplices vs negatively oriented ones. - match robust_orientation(&points) { - Ok(Orientation::POSITIVE) => Ok(1), - Ok(Orientation::NEGATIVE) => Ok(-1), - Ok(Orientation::DEGENERATE) => Ok(0), - Err(e) => Err(TdsError::InconsistentDataStructure { - message: format!( - "{predicate_failure_prefix} {:?} (key {simplex_key:?}): {e}", - simplex.uuid(), - ), - }), - } - } - /// Validates geometric orientation sign for each stored simplex using exact arithmetic - /// via [`robust_orientation`]. - /// - /// Simplices are stored in canonical positive orientation order by construction and mutation - /// paths; a negative sign indicates geometric/combinatorial mismatch. - /// - /// Orientation is evaluated through [`evaluate_simplex_orientation_for_context`](Self::evaluate_simplex_orientation_for_context), - /// which uses [`robust_orientation`] exclusively (no `SoS`) so that truly degenerate - /// simplices are correctly identified rather than masked. - /// - /// Periodic-lifted simplices are validated in lifted coordinates using per-vertex periodic - /// offsets and toroidal domain periods. - pub(crate) fn validate_geometric_simplex_orientation(&self) -> Result<(), TdsError> { - for (simplex_key, simplex) in self.tds.simplices() { - let orientation = self.evaluate_simplex_orientation_for_context( - simplex_key, - simplex, - "geometric orientation validation", - "Geometric orientation predicate failed for simplex", - )?; - // Degenerate simplices (zero exact determinant) can legitimately arise - // from flip-based repair in higher dimensions. They are topologically - // valid (BFS coherent orientation handles them) and do not indicate - // a sign mismatch. Only flag simplices with negative orientation. - if orientation < 0 { - // Emit structured diagnostic context for debugging (especially 4D+ cases). - let vertex_keys: SmallBuffer = - simplex.vertices().iter().copied().collect(); - let neighbor_keys: SmallBuffer, MAX_PRACTICAL_DIMENSION_SIZE> = - simplex - .neighbor_keys() - .map(Iterator::collect) - .unwrap_or_default(); - tracing::debug!( - simplex_uuid = %simplex.uuid(), - ?simplex_key, - ?vertex_keys, - ?neighbor_keys, - orientation, - "negative geometric orientation detected during validation", - ); - - return Err(TdsError::Geometric(GeometricError::NegativeOrientation { - message: format!( - "Simplex {:?} (key {simplex_key:?}, vertices {vertex_keys:?}) has negative geometric orientation; expected positive canonical orientation", - simplex.uuid(), - ), - })); - } - } - - Ok(()) - } - - /// Validates geometric orientation for a local set of simplices. - fn validate_geometric_simplex_orientation_for_simplices( - &self, - simplices: &[SimplexKey], - ) -> Result<(), TdsError> { - for &simplex_key in simplices { - let simplex = - self.tds - .simplex(simplex_key) - .ok_or_else(|| TdsError::SimplexNotFound { - simplex_key, - context: "local geometric orientation validation scope".to_string(), - })?; - let orientation = self.evaluate_simplex_orientation_for_context( - simplex_key, - simplex, - "local geometric orientation validation", - "Geometric orientation predicate failed for local simplex", - )?; - if orientation < 0 { - let vertex_keys: SmallBuffer = - simplex.vertices().iter().copied().collect(); - tracing::debug!( - simplex_uuid = %simplex.uuid(), - ?simplex_key, - ?vertex_keys, - orientation, - "negative geometric orientation detected during local validation", - ); - - return Err(TdsError::Geometric(GeometricError::NegativeOrientation { - message: format!( - "Simplex {:?} (key {simplex_key:?}, vertices {vertex_keys:?}) has negative geometric orientation; expected positive canonical orientation", - simplex.uuid(), - ), - })); - } - } - - Ok(()) - } - - /// Validates local orientation invariants for simplices changed by insertion. - fn validate_local_orientation_for_simplices( - &self, - simplices: &[SimplexKey], - ) -> Result<(), InsertionError> { - self.tds - .validate_coherent_orientation_for_simplices(simplices)?; - self.validate_geometric_simplex_orientation_for_simplices(simplices)?; - Ok(()) - } - - /// Flip all negatively oriented simplices to positive orientation. - /// - /// This applies to both Euclidean simplices and periodic-lifted simplices (when present). - /// - /// Returns `true` if at least one simplex was flipped. - fn promote_simplices_to_positive_orientation(&mut self) -> Result { - let mut negative_simplices = SimplexKeyBuffer::new(); - - for (simplex_key, simplex) in self.tds.simplices() { - let orientation = self.evaluate_simplex_orientation_for_context( - simplex_key, - simplex, - "positive-orientation promotion", - "Geometric orientation predicate failed while promoting positive orientation for simplex", - )?; - // Skip degenerate simplices — their exact determinant is zero, so there - // is no meaningful "positive" to promote to. BFS coherent-orientation - // normalization and the global sign canonicalization handle them. - if orientation == 0 { - continue; - } - if orientation < 0 { - negative_simplices.push(simplex_key); - } - } - - if negative_simplices.is_empty() { - return Ok(false); - } - - for simplex_key in negative_simplices { - let simplex = - self.tds - .simplex_mut(simplex_key) - .ok_or_else(|| TdsError::SimplexNotFound { - simplex_key, - context: "applying positive-orientation promotion".to_string(), - })?; - if simplex.number_of_vertices() >= 2 { - simplex.swap_vertex_slots(0, 1); - } - } - - self.tds.mark_topology_modified(); - Ok(true) - } - - /// Check whether any simplex still requires positive-orientation promotion. - /// - /// This performs the same orientation inspection as promotion, but does not mutate any simplices. - /// - /// # Returns - /// - /// - `Ok(true)` if at least one simplex has negative geometric orientation. - /// - `Ok(false)` if all simplices are already positively oriented. - /// - /// # Errors - /// - /// Returns an [`InsertionError`] if orientation evaluation fails. - /// Geometrically degenerate simplices (`orientation == 0` per [`robust_orientation`]) - /// are skipped, consistent with [`promote_simplices_to_positive_orientation`](Self::promote_simplices_to_positive_orientation). - fn simplices_require_positive_orientation_promotion(&self) -> Result { - for (simplex_key, simplex) in self.tds.simplices() { - let orientation = self.evaluate_simplex_orientation_for_context( - simplex_key, - simplex, - "positive-orientation convergence check", - "Geometric orientation predicate failed while checking positive-orientation convergence for simplex", - )?; - // Skip degenerate simplices (see promote_simplices_to_positive_orientation). - if orientation == 0 { - continue; - } - if orientation < 0 { - return Ok(true); - } - } - - Ok(false) - } - - /// For connected non-periodic triangulations, coherent orientation has two equivalent global - /// sign choices. Canonicalize that global sign to positive by flipping all simplices when needed. - fn canonicalize_global_orientation_sign(&mut self) -> Result<(), InsertionError> { - // Find the first simplex with a non-zero exact orientation - // Skip degenerate simplices (orientation == 0) — they have no meaningful geometric sign. - let representative_sign = { - let mut sign = None; - for (simplex_key, simplex) in self.tds.simplices() { - let orientation = self.evaluate_simplex_orientation_for_context( - simplex_key, - simplex, - "global orientation-sign canonicalization", - "Geometric orientation predicate failed while canonicalizing global orientation sign for simplex", - )?; - if orientation != 0 { - sign = Some(orientation); - break; - } - } - sign - }; - - if representative_sign != Some(-1) { - return Ok(()); - } - - let simplex_keys: SimplexKeyBuffer = self.tds.simplex_keys().collect(); - let mut flipped_any = false; - for simplex_key in simplex_keys { - let Some(simplex) = self.tds.simplex_mut(simplex_key) else { - continue; - }; - if simplex.number_of_vertices() >= 2 { - simplex.swap_vertex_slots(0, 1); - flipped_any = true; - } - } - - if flipped_any { - self.tds.mark_topology_modified(); - } - - Ok(()) - } - - /// Normalize coherent orientation and promote geometric orientation to the positive - /// canonical sign. - /// - /// Strategy: - /// 1. Normalize coherent orientation (BFS propagation) so all adjacencies agree. - /// 2. Canonicalize the global sign — for a connected orientable manifold all simplices - /// share the same sign after normalization, so a single global flip resolves it. - /// 3. Fall back to bounded per-simplex promotion passes for FP-precision edge cases. - pub(crate) fn normalize_and_promote_positive_orientation( - &mut self, - ) -> Result<(), InsertionError> { - // Phase 1: make all adjacencies coherent - // Canonicalizing *before* the promote loop resolves the common case where - // all simplices share the same (negative) sign after BFS normalization. - self.tds.normalize_coherent_orientation()?; - self.canonicalize_global_orientation_sign()?; - - // Phase 2 (fallback): bounded promote + normalize passes for stragglers. - for _ in 0..3 { - if !self.promote_simplices_to_positive_orientation()? { - break; - } - self.tds.normalize_coherent_orientation()?; - } - - // Soft post-condition: after normalize + canonicalize + bounded promote - // passes, any remaining "negative" simplices are near-degenerate (det ≈ 0) - // where the fast kernel's sign is unreliable. Log a diagnostic but do - // not fail — the BFS normalization guarantees coherent orientation and - // the global canonicalization ensures the dominant sign is positive. - if self.simplices_require_positive_orientation_promotion()? { - let mut residual_count = 0_usize; - let mut sample_keys: [Option; 5] = [None; 5]; - for (simplex_key, simplex) in self.tds.simplices() { - let orientation = self.evaluate_simplex_orientation_for_context( - simplex_key, - simplex, - "residual negative-orientation sampling", - "Geometric orientation predicate failed while sampling residual negatives for simplex", - )?; - if orientation < 0 { - if residual_count < sample_keys.len() { - sample_keys[residual_count] = Some(simplex_key); - } - residual_count += 1; - } - } - let sampled: Vec = sample_keys.into_iter().flatten().collect(); - tracing::debug!( - residual_count, - sampled_keys = ?sampled, - "normalize_and_promote_positive_orientation: \ - {residual_count} simplices still appear negative after bounded promotion \ - passes (likely near-degenerate FP noise); accepting coherent orientation" - ); - } - self.canonicalize_global_orientation_sign()?; - Ok(()) - } - /// Canonicalize a set of newly created simplices to positive geometric orientation. - /// - /// This preserves simplex-local slot alignment (vertices/neighbors/periodic offsets) by using - /// `swap_vertex_slots(0, 1)` for negatively oriented simplices. - #[expect( - clippy::too_many_lines, - reason = "debug-only orientation diagnostics with dedup add conditional branches" - )] - fn canonicalize_positive_orientation_for_simplices( - &mut self, - simplices: &SimplexKeyBuffer, - ) -> Result<(), InsertionError> { - #[cfg(debug_assertions)] - let debug_orientation = std::env::var_os("DELAUNAY_DEBUG_ORIENTATION").is_some(); - #[cfg(debug_assertions)] - let mut orientation_warn_count = 0_usize; - - for &simplex_key in simplices { - let orientation = { - let simplex = - self.tds - .simplex(simplex_key) - .ok_or_else(|| TdsError::SimplexNotFound { - simplex_key, - context: "canonicalizing insertion orientation".to_string(), - })?; - self.evaluate_simplex_orientation_for_context( - simplex_key, - simplex, - "insertion orientation canonicalization", - "Geometric orientation predicate failed while canonicalizing simplex", - )? - }; - - if orientation == 0 { - // Keep temporary degenerate simplices unchanged here. Downstream local-repair and - // topology/geometry validation decide whether they are removed or rejected. - continue; - } - - if orientation < 0 { - // Capture pre-swap vertices only when the debug env var is set, - // so release and non-debug runs avoid the allocation entirely. - #[cfg(debug_assertions)] - let pre_swap_vertices = if debug_orientation { - self.tds.simplex(simplex_key).map(|c| c.vertices().to_vec()) - } else { - None - }; - - let simplex = - self.tds - .simplex_mut(simplex_key) - .ok_or_else(|| TdsError::SimplexNotFound { - simplex_key, - context: "applying insertion orientation canonicalization".to_string(), - })?; - if simplex.number_of_vertices() < 2 { - return Err(TdsError::DimensionMismatch { - expected: 2, - actual: simplex.number_of_vertices(), - context: format!( - "simplex {simplex_key:?} needs >= 2 vertices for orientation canonicalization" - ), - } - .into()); - } - simplex.swap_vertex_slots(0, 1); - - #[cfg(debug_assertions)] - if debug_orientation { - orientation_warn_count += 1; - // Log full detail for the first 3 occurrences; suppress the rest. - if orientation_warn_count <= 3 { - // Re-evaluate orientation after swap to confirm it worked. - // Handle the Result locally so verification failures are - // observational only and never promote to insertion errors. - let post_orientation = self.tds.simplex(simplex_key).map(|c| { - self.evaluate_simplex_orientation_for_context( - simplex_key, - c, - "orientation swap verification", - "orientation predicate failed during swap verification", - ) - }); - match post_orientation { - Some(Ok(post_o)) => { - tracing::warn!( - simplex_key = ?simplex_key, - pre_swap_vertices = ?pre_swap_vertices, - pre_swap_orientation = orientation, - post_swap_orientation = post_o, - swap_fixed = post_o > 0, - "canonicalize_positive_orientation: negative-orientation simplex swapped" - ); - } - Some(Err(ref e)) => { - tracing::warn!( - simplex_key = ?simplex_key, - pre_swap_vertices = ?pre_swap_vertices, - pre_swap_orientation = orientation, - error = %e, - "canonicalize_positive_orientation: post-swap verification failed" - ); - } - None => { - tracing::warn!( - simplex_key = ?simplex_key, - pre_swap_vertices = ?pre_swap_vertices, - pre_swap_orientation = orientation, - "canonicalize_positive_orientation: simplex not found after swap" - ); - } - } - } - } - } - } - - #[cfg(debug_assertions)] - if orientation_warn_count > 3 && debug_orientation { - let suppressed = orientation_warn_count - 3; - tracing::warn!( - total_negative = orientation_warn_count, - suppressed, - "canonicalize_positive_orientation: suppressed {suppressed} additional negative-orientation warnings (see first 3 above)" - ); - } - - Ok(()) - } - - /// Validates topological invariants of the triangulation (Level 3). - /// - /// This checks the triangulation/topology layer **only**: - /// - Codimension-1 pseudomanifold condition: each facet is incident to 1 (boundary) or 2 (interior) simplices - /// - Codimension-2 boundary manifoldness: the boundary must be closed ("no boundary of boundary") - /// - Geometric orientation-sign consistency for stored simplices (signed determinant > 0) - /// - Ridge-link validation (when `topology_guarantee.requires_ridge_links()`) - /// - Vertex-link validation during insertion (when `topology_guarantee.requires_vertex_links_during_insertion()`) - /// - Connectedness (single component in the simplex neighbor graph) - /// - No isolated vertices (every vertex must be incident to at least one simplex) - /// - Euler characteristic - /// - /// For `TopologyGuarantee::PLManifold`, full PL-manifold certification requires - /// calling [`Triangulation::validate_at_completion`](Self::validate_at_completion) - /// (or [`Triangulation::validate`](Self::validate)) after batch construction. - /// - /// It intentionally does **not** validate lower layers (vertices/simplices or TDS structure). - /// For cumulative validation, use [`Triangulation::validate`](Self::validate). - /// - /// # Errors - /// - /// Returns an [`InvariantError`] if: - /// - The manifold-with-boundary facet property is violated. - /// - The triangulation is disconnected (multiple simplex components). - /// - An isolated vertex is detected (no incident simplex). - /// - Euler characteristic validation fails. - /// - The topology module reports an error (treated as inconsistent data structure). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// let vertices_4d = [ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 4> = - /// DelaunayTriangulation::new(&vertices_4d).unwrap(); - /// - /// // Level 3: topology validation (manifold-with-boundary + Euler characteristic) - /// assert!(dt.as_triangulation().is_valid().is_ok()); - /// ``` - pub fn is_valid(&self) -> Result<(), InvariantError> { - self.validate_topology_core()?; - // Check geometric orientation after manifold/link checks so topology-specific - // diagnostics surface first when multiple invariants are violated. - self.validate_geometric_simplex_orientation()?; - Ok(()) - } - - /// Validates topological invariants **without** geometric orientation checks. - /// - /// This is identical to [`is_valid`](Self::is_valid) but omits the - /// `validate_geometric_simplex_orientation()` step. It is intended for - /// explicit combinatorial construction where the user-provided vertex - /// orderings may produce negative determinants that are nonetheless - /// topologically valid. - pub(crate) fn is_valid_topology_only(&self) -> Result<(), InvariantError> { - self.validate_topology_core() - } - - /// Verifies that no simplex is geometrically degenerate (zero-volume simplex). - /// - /// This is a sign-agnostic check: it flags simplices whose exact orientation - /// determinant is zero (collinear in 2D, coplanar in 3D, etc.) regardless - /// of the sign. Intended for explicit construction where the user-supplied - /// vertex set must form non-degenerate simplices. - /// - /// Unlike [`validate_geometric_simplex_orientation`](Self::validate_geometric_simplex_orientation), - /// this does **not** reject negative-orientation simplices. - pub(crate) fn validate_geometric_nondegeneracy(&self) -> Result<(), TdsError> { - for (simplex_key, simplex) in self.tds.simplices() { - let orientation = self.evaluate_simplex_orientation_for_context( - simplex_key, - simplex, - "geometric nondegeneracy check", - "Orientation predicate failed for simplex", - )?; - if orientation == 0 { - return Err(TdsError::Geometric(GeometricError::DegenerateOrientation { - message: format!( - "Simplex {:?} (key {simplex_key:?}) is geometrically degenerate \ - (zero-volume simplex from collinear/coplanar vertices)", - simplex.uuid(), - ), - })); - } - } - Ok(()) - } - - /// Shared Level-3 topology validation sequence used by both [`is_valid`](Self::is_valid) - /// and [`is_valid_topology_only`](Self::is_valid_topology_only). - /// - /// Checks connectedness, manifold facet degree, closed boundary, ridge/vertex - /// links (when required by the topology guarantee), isolated vertices, and - /// Euler characteristic. - fn validate_topology_core(&self) -> Result<(), InvariantError> { - // 1. Connectedness - // - // Checked first because it is cheaper than building the facet-to-simplices map - // (which requires O(N·D) hash-map insertions plus allocations) and avoids - // all subsequent work when the triangulation is disconnected. - self.validate_global_connectedness()?; - - // 2. Manifold facet multiplicity (codimension-1 pseudomanifold condition) - // - // Build the facet map once and reuse it for manifold validation and Euler counting. - let facet_to_simplices: FacetToSimplicesMap = self.tds.build_facet_to_simplices_map()?; - self.validate_topology_core_with_facet_to_simplices_map(&facet_to_simplices) - } - - fn validate_topology_core_with_facet_to_simplices_map( - &self, - facet_to_simplices: &FacetToSimplicesMap, - ) -> Result<(), InvariantError> { - validate_facet_degree(facet_to_simplices)?; - - // 2b. Boundary manifoldness in codimension 2: the boundary must be "closed" - // (i.e., its ridges must have degree 2 within boundary facets). - validate_closed_boundary(&self.tds, facet_to_simplices)?; - - // 2c. Ridge-link validation for PLManifold/PLManifoldStrict (fast, catches many PL issues). - if self.topology_guarantee.requires_ridge_links() { - validate_ridge_links(&self.tds)?; - } - // 2d. PL-manifold vertex-link condition during insertion (strict mode). - if self - .topology_guarantee - .requires_vertex_links_during_insertion() - { - validate_vertex_links(&self.tds, facet_to_simplices)?; - } - - // 3. Vertex incidence (manifold invariant): every vertex must be incident to at least one simplex. - self.validate_no_isolated_vertices()?; - - // 4. Euler characteristic using the topology module - let topology_result = - validate_triangulation_euler_with_facet_to_simplices_map(&self.tds, facet_to_simplices); - - // Override the heuristic classification when the caller has declared a - // non-Euclidean global topology. The heuristic classifies any closed - // mesh (no boundary facets) as `ClosedSphere(D)`, but a toroidal mesh - // also has no boundary — its expected χ is 0, not 1+(-1)^D. - let (classification, expected) = match self.global_topology { - GlobalTopology::Toroidal { .. } - if matches!( - topology_result.classification, - TopologyClassification::ClosedSphere(_) - ) => - { - let cls = TopologyClassification::ClosedToroid(D); - (cls, expected_chi_for(&cls)) - } - _ => (topology_result.classification, topology_result.expected), - }; - - if let Some(exp) = expected - && topology_result.chi != exp - { - return Err(TriangulationValidationError::EulerCharacteristicMismatch { - computed: topology_result.chi, - expected: exp, - classification, - } - .into()); - } - - Ok(()) - } - - /// Validates vertex-link condition at construction completion. - /// - /// This should be called once after batch construction is complete to certify - /// full PL-manifoldness when using `TopologyGuarantee::PLManifold` (incremental mode). - /// - /// # Errors - /// - /// Returns an [`InvariantError`] if vertex-link validation fails - /// (e.g. a vertex link is not a PL-sphere/ball as required for PL-manifoldness). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// 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::new(&vertices).unwrap(); - /// assert!(dt.as_triangulation().validate_at_completion().is_ok()); - /// ``` - pub fn validate_at_completion(&self) -> Result<(), InvariantError> { - if !self - .topology_guarantee - .requires_vertex_links_at_completion() - { - return Ok(()); - } - - if self.tds.number_of_simplices() == 0 { - return Ok(()); - } - - let facet_to_simplices: FacetToSimplicesMap = self.tds.build_facet_to_simplices_map()?; - self.validate_at_completion_with_facet_to_simplices_map(&facet_to_simplices)?; - Ok(()) - } - - fn validate_at_completion_with_facet_to_simplices_map( - &self, - facet_to_simplices: &FacetToSimplicesMap, - ) -> Result<(), InvariantError> { - if !self - .topology_guarantee - .requires_vertex_links_at_completion() - { - return Ok(()); - } - - if self.tds.number_of_simplices() == 0 { - return Ok(()); - } - - validate_vertex_links(&self.tds, facet_to_simplices)?; - Ok(()) - } - - /// Performs cumulative validation for Levels 1–3. - /// - /// This validates: - /// - **Level 1–2** via [`Tds::validate`](crate::core::tds::Tds::validate) - /// - **Level 3** via [`Triangulation::is_valid`](Self::is_valid) - /// - **Completion-time PL-manifold check** via [`Triangulation::validate_at_completion`](Self::validate_at_completion) - /// - /// # Errors - /// - /// Returns an [`InvariantError`] if: - /// - Any vertex/simplex is invalid (Level 1). - /// - The TDS structural invariants fail (Level 2). - /// - Topology validation fails (Level 3). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// let vertices_4d = [ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 4> = - /// DelaunayTriangulation::new(&vertices_4d).unwrap(); - /// - /// // Levels 1–3: elements + TDS structure + topology - /// assert!(dt.as_triangulation().validate().is_ok()); - /// ``` - pub fn validate(&self) -> Result<(), InvariantError> - where - U: DataType, - V: DataType, - { - self.tds.validate()?; - self.validate_global_connectedness()?; - let facet_to_simplices: FacetToSimplicesMap = self.tds.build_facet_to_simplices_map()?; - self.validate_topology_core_with_facet_to_simplices_map(&facet_to_simplices)?; - // Check geometric orientation after manifold/link checks so topology-specific - // diagnostics surface first when multiple invariants are violated. - self.validate_geometric_simplex_orientation()?; - self.validate_at_completion_with_facet_to_simplices_map(&facet_to_simplices) - } - - /// Generate a comprehensive validation report for Levels 1–3. - /// - /// This is intended for debugging/telemetry where you want to see *all* violated - /// invariants, not just the first one. - /// - /// # Notes - /// - If UUID↔key mappings are inconsistent, this returns only mapping failures (other - /// checks may produce misleading secondary errors). - /// - This report is **cumulative** across Levels 1–3. - /// - /// # Errors - /// - /// Returns `Err(TriangulationValidationReport)` containing all invariant violations. - pub(crate) fn validation_report(&self) -> Result<(), TriangulationValidationReport> - where - U: DataType, - V: DataType, - { - let mut violations: Vec = Vec::new(); - - // Level 2 (structural): reuse the TDS report. - match self.tds.validation_report() { - Ok(()) => {} - Err(report) => { - if report.violations.iter().any(|v| { - matches!( - v.kind, - InvariantKind::VertexMappings | InvariantKind::SimplexMappings - ) - }) { - return Err(report); - } - violations.extend(report.violations); - } - } - - // Level 1 (element validity): vertices - for (_vertex_key, vertex) in self.tds.vertices() { - if let Err(source) = (*vertex).is_valid() { - violations.push(InvariantViolation { - kind: InvariantKind::VertexValidity, - error: InvariantError::Tds(TdsError::InvalidVertex { - vertex_id: vertex.uuid(), - source, - }), - }); - } - } - - // Level 1 (element validity): simplices - for (_simplex_key, simplex) in self.tds.simplices() { - if let Err(source) = simplex.is_valid() { - violations.push(InvariantViolation { - kind: InvariantKind::SimplexValidity, - error: InvariantError::Tds(TdsError::InvalidSimplex { - simplex_id: simplex.uuid(), - source, - }), - }); - } - } - - // Level 3 (topology) - if let Err(e) = self.is_valid() { - violations.push(InvariantViolation { - kind: InvariantKind::Topology, - error: e, - }); - } - - if violations.is_empty() { - Ok(()) - } else { - Err(TriangulationValidationReport { violations }) - } - } - - /// Validates that the triangulation's simplex neighbor graph is a single connected component. - /// - /// Delegates to [`Tds::is_connected`], an O(N·D) BFS over neighbor pointers. - fn validate_global_connectedness(&self) -> Result<(), TriangulationValidationError> { - if !self.tds.is_connected() { - return Err(TriangulationValidationError::Disconnected { - simplex_count: self.tds.number_of_simplices(), - }); - } - Ok(()) - } - - /// Validates that every vertex is incident to at least one simplex. - /// - /// Isolated vertices are allowed at the TDS (structural) layer, but they violate the - /// manifold invariants checked at the topology (Level 3) layer. - fn validate_no_isolated_vertices(&self) -> Result<(), TriangulationValidationError> { - if self.tds.number_of_vertices() == 0 { - return Ok(()); - } - - let mut vertices_in_simplices: FastHashSet = - fast_hash_set_with_capacity(self.tds.number_of_vertices()); - - for (_simplex_key, simplex) in self.tds.simplices() { - for &vk in simplex.vertices() { - vertices_in_simplices.insert(vk); - } - } - - for (vk, vertex) in self.tds.vertices() { - if !vertices_in_simplices.contains(&vk) { - return Err(TriangulationValidationError::IsolatedVertex { - vertex_key: vk, - vertex_uuid: vertex.uuid(), - }); - } - } - - Ok(()) - } -} - -// ============================================================================= -// Geometric Operations (Requires Extra Numeric Conversion Bounds) -// ============================================================================= - -impl Triangulation -where - K: Kernel, - K::Scalar: NumCast, - U: DataType, - V: DataType, -{ - /// Build initial D-simplex from D+1 vertices with degeneracy validation. - /// - /// This creates a Tds with a single simplex containing all D+1 vertices, - /// with explicit boundary neighbor slots. The simplex is - /// validated to ensure it is non-degenerate (vertices span full D-dimensional space). - /// - /// **Design Note**: This method uses [`robust_orientation`] directly for the - /// non-degeneracy check, bypassing the kernel. This avoids `SoS` tie-breaking - /// (which would mask truly degenerate input) and keeps the method independent - /// of kernel state. - /// - /// # Arguments - /// - `vertices`: Exactly D+1 vertices to form the initial simplex - /// - /// # Returns - /// A Tds containing one D-simplex with all vertices, ready for incremental insertion. - /// - /// # Errors - /// Returns error if: - /// - Wrong number of vertices (must be exactly D+1) - /// - Vertices are degenerate (collinear in 2D, coplanar in 3D, etc.) - /// - Vertex or simplex insertion fails - /// - Duplicate UUIDs detected - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::*; - /// use delaunay::prelude::triangulation::*; - /// - /// // Create a 2D triangle (initial simplex) - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let tds = Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - /// assert_eq!(tds.number_of_vertices(), 3); - /// assert_eq!(tds.number_of_simplices(), 1); - /// assert_eq!(tds.dim(), 2); - /// - /// // Error: wrong number of vertices (need exactly D+1) - /// let bad_vertices = vec![vertex!([0.0, 0.0])]; - /// let result = Triangulation::, (), (), 2>::build_initial_simplex(&bad_vertices); - /// assert!(result.is_err()); - /// - /// // Error: collinear points in 2D (degenerate simplex) - /// let collinear = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([2.0, 0.0]), - /// ]; - /// let result = Triangulation::, (), (), 2>::build_initial_simplex(&collinear); - /// assert!(result.is_err()); - /// ``` - pub fn build_initial_simplex( - vertices: &[Vertex], - ) -> Result, TriangulationConstructionError> { - if vertices.len() != D + 1 { - return Err(TriangulationConstructionError::InsufficientVertices { - dimension: D, - source: SimplexValidationError::InsufficientVertices { - actual: vertices.len(), - expected: D + 1, - dimension: D, - }, - }); - } - - for vertex in vertices { - vertex.is_valid().map_err(|source| { - TriangulationConstructionError::Tds(TdsConstructionError::ValidationError( - TdsError::InvalidVertex { - vertex_id: vertex.uuid(), - source, - }, - )) - })?; - } - - // Validate that the simplex is non-degenerate using exact orientation. - // Use robust_orientation (no SoS) so that truly degenerate input - // (collinear/coplanar) is detected even when the kernel uses SoS. - - // Collect points into stack-allocated buffer (at most 8 points for D ≤ 7) - let points: SmallBuffer, MAX_PRACTICAL_DIMENSION_SIZE> = - vertices.iter().map(|v| *v.point()).collect(); - - // Exact degeneracy check — DEGENERATE means zero-volume simplex. - let exact_orientation = robust_orientation(&points[..]).map_err(|e| { - TriangulationConstructionError::FailedToCreateSimplex { - message: format!("Exact orientation test failed: {e}"), - } - })?; - - if matches!(exact_orientation, Orientation::DEGENERATE) { - return Err(TriangulationConstructionError::GeometricDegeneracy { - message: format!( - "Degenerate initial simplex: vertices are collinear/coplanar in {}D space. \ - The {} input vertices do not span a full {}-dimensional simplex. \ - Provide non-degenerate vertices to create a valid triangulation.", - D, - D + 1, - D - ), - }); - } - - // Use the exact orientation sign directly — robust_orientation already - // provides a provably correct POSITIVE/NEGATIVE for non-degenerate inputs. - let orientation = match exact_orientation { - Orientation::POSITIVE => 1, - Orientation::NEGATIVE => -1, - Orientation::DEGENERATE => { - // Unreachable: degeneracy was checked above. - return Err(TriangulationConstructionError::GeometricDegeneracy { - message: format!("Degenerate initial simplex in {D}D (unreachable)"), - }); - } - }; - - // Create empty Tds - let mut tds = Tds::empty(); - - // Insert all vertices and collect their keys - let mut vertex_keys = SmallBuffer::::new(); - for vertex in vertices { - let vkey = tds.insert_vertex_with_mapping(*vertex)?; - vertex_keys.push(vkey); - } - - // Canonicalize initial simplex orientation: store simplices in positive orientation order. - // Swapping any two vertices flips orientation. - if orientation < 0 { - if vertex_keys.len() >= 2 { - vertex_keys.swap(0, 1); - } else { - return Err(TriangulationConstructionError::FailedToCreateSimplex { - message: format!( - "Cannot canonicalize orientation for {}D simplex with {} vertex key(s)", - D, - vertex_keys.len(), - ), - }); - } - } - - // Create single D-simplex from all vertices in canonicalized order. - let simplex = Simplex::new(vertex_keys, None).map_err(|e| { - TriangulationConstructionError::FailedToCreateSimplex { - message: format!("Failed to create initial simplex: {e}"), - } - })?; - - // Insert the simplex - let _simplex_key = tds.insert_simplex_with_mapping(simplex)?; - - // Assign explicit boundary neighbor slots for the initial simplex. - tds.assign_neighbors() - .map_err(TdsConstructionError::ValidationError)?; - - // Assign incident simplices to vertices (each vertex points to this one simplex) - // This is required for proper Tds structure - tds.assign_incident_simplices() - .map_err(|e| TdsConstructionError::ValidationError(e.into()))?; - - Ok(tds) - } - - /// Insert a vertex into the triangulation using cavity-based algorithm. - /// - /// This is a generic insertion method that handles: - /// - **Bootstrap (< D+1 vertices)**: Accumulates vertices without creating simplices - /// - **Initial simplex (D+1 vertices)**: Automatically builds the first D-simplex - /// - **Incremental (> D+1 vertices)**: Cavity-based insertion or hull extension - /// - /// # Arguments - /// - `vertex`: The vertex to insert - /// - `conflict_simplices`: Optional conflict region (simplices to be removed). Required for - /// interior points, not needed for exterior points (hull extension). - /// - `hint`: Optional simplex hint for point location (improves performance) - /// - /// # Algorithm - /// 1. Insert vertex into Tds - /// 2. Check vertex count: - /// - If < D+1: Return (bootstrap phase) - /// - If == D+1: Build initial simplex from all vertices - /// - If > D+1: Continue with steps 3-7 - /// 3. Locate simplex containing the point - /// 4. Handle location result: - /// - `InsideSimplex`: Use provided `conflict_simplices` for cavity-based insertion - /// - `Outside`: Extend hull (no conflict simplices needed) - /// 5. Extract cavity boundary (if interior) - /// 6. Fill cavity (create new simplices) - /// 7. Wire neighbors locally - /// 8. Remove conflict simplices (if interior) - /// 9. Repair invalid facet sharing - /// - /// # Returns - /// - `Ok(VertexKey)`: The key of the inserted vertex - /// - New simplex keys via the returned result (for hint caching at higher layers) - /// - /// # Errors - /// Returns error if: - /// - Duplicate coordinates detected (within 1e-10 tolerance) - /// - Duplicate UUID detected - /// - Initial simplex construction fails - /// - Point location fails - /// - Interior point without `conflict_simplices` parameter - /// - Cavity operations fail - /// - Degenerate location (`OnFacet`, `OnEdge`, `OnVertex`) - not yet implemented - /// - /// **Note**: For insertions beyond D+1 vertices, use `DelaunayTriangulation::insert()` - /// instead, which handles conflict region computation automatically. - #[cfg(test)] - pub(crate) fn insert( - &mut self, - vertex: Vertex, - conflict_simplices: Option<&SimplexKeyBuffer>, - hint: Option, - ) -> Result<(VertexKey, Option), InsertionError> { - let (outcome, _stats) = self.insert_transactional( - vertex, - conflict_simplices, - hint, - DEFAULT_PERTURBATION_RETRIES, - 0, - None, - None, - )?; - match outcome { - InsertionOutcome::Inserted { vertex_key, hint } => Ok((vertex_key, hint)), - InsertionOutcome::Skipped { error } => Err(error), - } - } - - /// Insert a vertex and return statistics about the operation. - /// - /// This method returns detailed statistics about the insertion including: - /// - Number of attempts (perturbation retries) - /// - Whether the vertex was skipped - /// - Number of simplices removed during repair - /// - /// This is useful for testing, debugging, and understanding how the - /// triangulation handles geometric degeneracies. - /// - /// # Errors - /// - /// Returns an error only for non-retryable structural failures (e.g. duplicate UUID). - /// Retryable geometric degeneracies that exhaust all attempts, and duplicate coordinates, - /// return `Ok((InsertionOutcome::Skipped { .. }, stats))`. - #[cfg(test)] - pub(crate) fn insert_with_statistics( - &mut self, - vertex: Vertex, - conflict_simplices: Option<&SimplexKeyBuffer>, - hint: Option, - ) -> Result<(InsertionOutcome, InsertionStatistics), InsertionError> { - self.insert_transactional( - vertex, - conflict_simplices, - hint, - DEFAULT_PERTURBATION_RETRIES, - 0, - None, - None, - ) - } - - /// Insert a vertex with statistics, using a custom perturbation seed and an optional - /// spatial hash-grid index, and also return the simplices that cavity reduction touched - /// and left in place. - /// - /// The extra seed set stays internal so bulk construction and debug rebuilds can widen - /// their local repair frontier without changing the public insertion API. - pub(crate) fn insert_with_statistics_seeded_indexed_detailed( - &mut self, - vertex: Vertex, - conflict_simplices: Option<&SimplexKeyBuffer>, - hint: Option, - perturbation_seed: u64, - index: Option<&mut HashGridIndex>, - bulk_index: Option, - ) -> Result { - self.insert_with_statistics_seeded_indexed_detailed_with_telemetry( - vertex, - conflict_simplices, - hint, - perturbation_seed, - index, - bulk_index, - InsertionTelemetryMode::CountsOnly, - ) - } - - /// Insert a vertex with statistics and explicitly selected telemetry collection. - /// - /// Use [`InsertionTelemetryMode::CountsAndTimings`] only when the caller will - /// consume elapsed-time telemetry; the default detailed insertion path records - /// counters without paying per-attempt `Instant::now()` costs. - #[expect( - clippy::too_many_arguments, - reason = "Internal detailed insertion carries perturbation, spatial-index, trace, and telemetry knobs" - )] - pub(crate) fn insert_with_statistics_seeded_indexed_detailed_with_telemetry( - &mut self, - vertex: Vertex, - conflict_simplices: Option<&SimplexKeyBuffer>, - hint: Option, - perturbation_seed: u64, - index: Option<&mut HashGridIndex>, - bulk_index: Option, - telemetry_mode: InsertionTelemetryMode, - ) -> Result { - self.insert_transactional_detailed( - vertex, - conflict_simplices, - hint, - DEFAULT_PERTURBATION_RETRIES, - perturbation_seed, - index, - bulk_index, - telemetry_mode, - ) - } - - /// Transactional insertion with automatic rollback and perturbation retry. - /// - /// This ensures the triangulation always remains in a valid state by: - /// 1. Cloning TDS before each insertion attempt (snapshot) - /// 2. Attempting insertion - /// 3. On failure: restore TDS from snapshot - /// 4. If the error is retryable: perturb vertex and retry (up to `max_perturbation_attempts`) - /// 5. If retryable attempts are exhausted, or the vertex is a duplicate: return - /// `Ok((InsertionOutcome::Skipped { error }, stats))` - /// 6. If the error is non-retryable: return `Err(InsertionError)` - /// - /// This guarantees we transition from one valid manifold to another. - #[cfg(test)] - #[expect( - clippy::too_many_arguments, - reason = "Test helpers mirror the detailed transactional insertion signature" - )] - fn insert_transactional( - &mut self, - vertex: Vertex, - conflict_simplices: Option<&SimplexKeyBuffer>, - hint: Option, - max_perturbation_attempts: usize, - perturbation_seed: u64, - index: Option<&mut HashGridIndex>, - bulk_index: Option, - ) -> Result<(InsertionOutcome, InsertionStatistics), InsertionError> { - let detail = self.insert_transactional_detailed( - vertex, - conflict_simplices, - hint, - max_perturbation_attempts, - perturbation_seed, - index, - bulk_index, - InsertionTelemetryMode::CountsOnly, - )?; - Ok((detail.outcome, detail.stats)) - } - - /// Transactional insertion with automatic rollback and perturbation retry, plus - /// the local-repair seed simplices discovered while shaping the cavity. - #[expect( - clippy::too_many_lines, - reason = "Complex insertion logic; splitting further would harm readability" - )] - #[expect( - clippy::too_many_arguments, - reason = "Transactional insertion needs the bulk-index diagnostic context for #204 tracing" - )] - fn insert_transactional_detailed( - &mut self, - vertex: Vertex, - conflict_simplices: Option<&SimplexKeyBuffer>, - hint: Option, - max_perturbation_attempts: usize, - perturbation_seed: u64, - mut index: Option<&mut HashGridIndex>, - bulk_index: Option, - telemetry_mode: InsertionTelemetryMode, - ) -> Result { - let mut stats = InsertionStatistics::default(); - let mut telemetry = InsertionTelemetry::default(); - let original_coords = *vertex.point().coords(); - let original_uuid = vertex.uuid(); - let mut current_vertex = vertex; - // Preserve the last retryable failure so an exhausted perturbation loop can - // explain why the vertex was skipped instead of reporting a generic error. - let mut last_retryable_error: Option = None; - - // Reuse the caller's spatial index as a locate-hint source when batch insertion did - // not already provide a better hint. This keeps retries and bulk runs on the same - // point-location path. - let mut hint = hint; - if hint.is_none() - && let Some(index_ref) = index.as_deref() - { - hint = self.select_locate_hint_from_hash_grid(&original_coords, index_ref); - } - - // Scale perturbations against the local neighborhood so retries stay small relative - // to the nearby geometry instead of using a single global epsilon. - let local_scale = self.estimate_local_perturbation_scale(&original_coords, hint); - - let duplicate_tolerance = - self.estimate_duplicate_coordinate_tolerance(&original_coords, hint); - self.ensure_duplicate_index_cell_size(index.as_deref_mut(), duplicate_tolerance); - - // Base perturbation epsilon: ≈ √machine_epsilon for the scalar type. - let epsilon_value: f64 = if K::Scalar::mantissa_digits() <= 24 { - 1e-4 - } else { - 1e-8 - }; - - for attempt in 0..=max_perturbation_attempts { - stats.attempts = attempt + 1; - - // Attempt 0 uses the caller's coordinates verbatim; later attempts apply a - // deterministic signed perturbation so the same seed reproduces the same path. - if attempt > 0 { - let mut perturbed_coords = original_coords; - // Progressive local-scale perturbation: magnitude grows ×10 per attempt. - // attempt 1: base × local_scale × 10 - // attempt 2: base × local_scale × 100 - // attempt 3: base × local_scale × 1000 - #[expect( - clippy::cast_possible_truncation, - clippy::cast_possible_wrap, - reason = "attempt is at most DEFAULT_PERTURBATION_RETRIES (3), fits in i32" - )] - let scale_factor = 10.0_f64.powi(attempt as i32); - let Some(epsilon) = ::from(epsilon_value * scale_factor) - else { - // We failed to convert the perturbation scale into the scalar type. - // - // This should not happen for our supported scalar types (`f32`, `f64`), but if it - // does (e.g. with a custom scalar), we degrade gracefully by skipping this vertex - // rather than aborting the whole insertion. - stats.result = InsertionResult::SkippedDegeneracy; - let error = last_retryable_error.unwrap_or_else(|| { - CavityFillingError::PerturbationScaleConversion { - value: epsilon_value.to_string(), - } - .into() - }); - return Ok(DetailedInsertionResult { - outcome: InsertionOutcome::Skipped { error }, - stats, - telemetry, - repair_seed_simplices: SimplexKeyBuffer::new(), - delaunay_repair_required: false, - }); - }; - - let perturbation_scale = epsilon * local_scale; - for (idx, coord) in perturbed_coords.iter_mut().enumerate() { - let coord_scale = - ::from(idx + 1).unwrap_or_else(K::Scalar::one); - let signed_perturbation = if perturbation_seed == 0 { - if (attempt + idx) % 2 == 0 { - perturbation_scale - } else { - -perturbation_scale - } - } else { - let mix = perturbation_seed - ^ ((attempt as u64) << 32) - ^ (idx as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15); - if mix & 1 == 0 { - perturbation_scale - } else { - -perturbation_scale - } - }; - *coord += signed_perturbation * coord_scale; - } - - // Preserve the caller-provided vertex UUID across perturbation retries. - // This ensures the inserted vertex retains its original identity even if we have - // to retry with perturbed coordinates. - current_vertex = - Vertex::new_with_uuid(Point::new(perturbed_coords), original_uuid, vertex.data); - } - - // Duplicate coordinate detection uses the hash grid when available; otherwise it - // falls back to a linear scan (O(n·D) per insertion, O(n²·D) worst-case). - if let Some(error) = self.duplicate_coordinates_error( - current_vertex.point().coords(), - duplicate_tolerance, - index.as_deref(), - ) { - stats.result = InsertionResult::SkippedDuplicate; - #[cfg(debug_assertions)] - tracing::debug!("SKIPPED: {error}"); - return Ok(DetailedInsertionResult { - outcome: InsertionOutcome::Skipped { error }, - stats, - telemetry, - repair_seed_simplices: SimplexKeyBuffer::new(), - delaunay_repair_required: false, - }); - } - - let simplices_before_attempt = self.tds.number_of_simplices(); - let vertices_before_attempt = self.tds.number_of_vertices(); - - // Clone TDS for rollback (transactional semantics) - let tds_snapshot = self.tds.clone_for_rollback(); - - // Try insertion. - // - // Topology safety net: ensure we don't commit an insertion that breaks Level 3 topology. - // If the cavity-based insertion produces an Euler/topology mismatch, roll back and retry a - // conservative fallback (star-split of the containing simplex) within the same transactional attempt. - #[cfg(test)] - // Test-only hook for deterministic coverage of the rollback + perturbation retry - // success path, which is otherwise rare under the adaptive SoS predicates. - let result = if test_hooks::take_force_next_insertion_retryable_failure() { - Err(InsertionError::NonManifoldTopology { - facet_hash: 0x000F_0CED, - simplex_count: 3, - }) - } else { - self.try_insert_with_topology_safety_net( - current_vertex, - conflict_simplices, - hint, - attempt, - &tds_snapshot, - &mut telemetry, - telemetry_mode, - ) - }; - #[cfg(not(test))] - let result = self.try_insert_with_topology_safety_net( - current_vertex, - conflict_simplices, - hint, - attempt, - &tds_snapshot, - &mut telemetry, - telemetry_mode, - ); - - match result { - Ok(TryInsertImplOk { - inserted, - simplices_removed, - repair_seed_simplices, - delaunay_repair_required, - .. - }) => { - stats.simplices_removed_during_repair = simplices_removed; - stats.result = InsertionResult::Inserted; - #[cfg(debug_assertions)] - if attempt > 0 { - tracing::debug!( - "Warning: Geometric degeneracy resolved via perturbation (attempt {attempt})" - ); - } - - let (vertex_key, hint) = inserted; - // Only the committed attempt updates the duplicate index. Earlier - // retries all rolled back to the pre-attempt triangulation state. - if let Some(index) = index.as_deref_mut() - && let Some(vertex) = self.tds.vertex(vertex_key) - { - index.insert_vertex(vertex_key, vertex.point().coords()); - } - - return Ok(DetailedInsertionResult { - outcome: InsertionOutcome::Inserted { vertex_key, hint }, - stats, - telemetry, - repair_seed_simplices, - delaunay_repair_required, - }); - } - Err(e) => { - // Any error - rollback to snapshot - self.tds = tds_snapshot; - - // Handle duplicate coordinates specially - skip immediately without retry - if matches!(e, InsertionError::DuplicateCoordinates { .. }) { - stats.result = InsertionResult::SkippedDuplicate; - #[cfg(debug_assertions)] - tracing::debug!("SKIPPED: {e}"); - return Ok(DetailedInsertionResult { - outcome: InsertionOutcome::Skipped { error: e }, - stats, - telemetry, - repair_seed_simplices: SimplexKeyBuffer::new(), - delaunay_repair_required: false, - }); - } - - // Check if this is a retryable error (geometric degeneracy) - let is_retryable = e.is_retryable(); - - // Emit the conflict summary after rollback so the trace captures the - // restored manifold state that the next retry will start from. - if retryable_skip_trace_enabled() - && let Some(detail) = retryable_conflict_trace_detail(&e) - { - log_retryable_conflict_skip( - bulk_index, - original_uuid, - attempt + 1, - max_perturbation_attempts + 1, - attempt > 0, - is_retryable && attempt < max_perturbation_attempts, - simplices_before_attempt, - vertices_before_attempt, - self.tds.number_of_simplices(), - self.tds.number_of_vertices(), - &detail, - &e, - ); - } - - if is_retryable && attempt < max_perturbation_attempts { - last_retryable_error = Some(e.clone()); - #[cfg(debug_assertions)] - tracing::debug!( - "RETRYING: Attempt {} failed with: {e}. Applying perturbation...", - attempt + 1 - ); - } else if is_retryable { - stats.result = InsertionResult::SkippedDegeneracy; - #[cfg(debug_assertions)] - tracing::debug!( - "SKIPPED: Could not insert vertex after {} attempts (max perturbation ≈ {:.0e} × local_scale). Last error: {e}. Vertex skipped to maintain manifold.", - max_perturbation_attempts + 1, - epsilon_value - * 10.0_f64.powi( - #[expect( - clippy::cast_possible_truncation, - clippy::cast_possible_wrap, - reason = "max_perturbation_attempts is small, fits in i32" - )] - { - max_perturbation_attempts as i32 - } - ), - ); - return Ok(DetailedInsertionResult { - outcome: InsertionOutcome::Skipped { error: e }, - stats, - telemetry, - // Skipped insertions do not mutate the triangulation, so any - // intermediate cavity-seed hints are irrelevant to callers. - repair_seed_simplices: SimplexKeyBuffer::new(), - delaunay_repair_required: false, - }); - } else { - // Non-retryable structural error (e.g., duplicate UUID) - return Err(e); - } - } - } - } - - Err(InsertionError::TopologyValidation( - TdsError::InconsistentDataStructure { - message: "insertion retry loop exhausted without producing an outcome".to_string(), - }, - )) - } - - fn select_locate_hint_from_hash_grid( - &self, - coords: &[K::Scalar; D], - index: &HashGridIndex, - ) -> Option { - let mut best: Option<(K::Scalar, SimplexKey)> = None; - - index.for_each_candidate_vertex_key(coords, |vkey| { - let Some(vertex) = self.tds.vertex(vkey) else { - return true; - }; - - let Some(simplex_key) = vertex.incident_simplex() else { - return true; - }; - - if !self.tds.contains_simplex(simplex_key) { - return true; - } - - let vcoords = vertex.point().coords(); - let mut dist_sq = K::Scalar::zero(); - for i in 0..D { - let diff = vcoords[i] - coords[i]; - dist_sq += diff * diff; - } - - match best { - Some((best_dist, _)) if dist_sq >= best_dist => {} - _ => { - best = Some((dist_sq, simplex_key)); - } - } - - true - }); - - best.map(|(_, simplex_key)| simplex_key) - } - - /// Chooses the relative duplicate-coordinate tolerance for the scalar precision. - fn duplicate_relative_tolerance() -> K::Scalar { - let value = if K::Scalar::mantissa_digits() <= 24 { - 1e-6_f64 - } else { - 1e-10_f64 - }; - ::from(value).unwrap_or_else(K::Scalar::default_tolerance) - } - - /// Keeps duplicate-scale estimates tied to existing geometry rather than - /// hard-coding a scalar-unit epsilon. - fn include_duplicate_scale_reference( - point_coords: &[K::Scalar; D], - axis_min: &mut [K::Scalar; D], - axis_max: &mut [K::Scalar; D], - magnitude_scale: &mut K::Scalar, - saw_reference: &mut bool, - ) { - *saw_reference = true; - for i in 0..D { - let coord = point_coords[i]; - if coord < axis_min[i] { - axis_min[i] = coord; - } - if coord > axis_max[i] { - axis_max[i] = coord; - } - - let abs = coord.abs(); - if abs > *magnitude_scale { - *magnitude_scale = abs; - } - } - } - - /// Estimates a duplicate-coordinate tolerance from the local simplex span plus - /// a small ULP-scaled floor for translated coordinate systems. - fn estimate_duplicate_coordinate_tolerance( - &self, - coords: &[K::Scalar; D], - hint: Option, - ) -> K::Scalar { - let mut axis_min = *coords; - let mut axis_max = *coords; - let mut magnitude_scale = K::Scalar::zero(); - let mut saw_reference = false; - let mut local_feature_scale = None; - - for coord in coords { - let abs = (*coord).abs(); - if abs > magnitude_scale { - magnitude_scale = abs; - } - } - - if let Some(simplex_key) = hint - && let Some(simplex) = self.tds.simplex(simplex_key) - { - for &vkey in simplex.vertices() { - if let Some(vertex) = self.tds.vertex(vkey) { - Self::include_duplicate_scale_reference( - vertex.point().coords(), - &mut axis_min, - &mut axis_max, - &mut magnitude_scale, - &mut saw_reference, - ); - } - } - } - - if !saw_reference { - let local_scale = self.estimate_local_perturbation_scale(coords, None); - if local_scale.is_finite() && local_scale > K::Scalar::zero() { - if local_scale > magnitude_scale { - magnitude_scale = local_scale; - } - local_feature_scale = Some(local_scale); - } - } - - let feature_scale = local_feature_scale.unwrap_or_else(|| { - let mut span_sq = K::Scalar::zero(); - for i in 0..D { - let span = axis_max[i] - axis_min[i]; - span_sq += span * span; - } - span_sq.sqrt() - }); - let relative_tolerance = Self::duplicate_relative_tolerance() * feature_scale; - let ulp_factor = ::from(16.0_f64).unwrap_or_else(K::Scalar::one); - let ulp_tolerance = K::Scalar::epsilon() * ulp_factor * magnitude_scale; - let mut tolerance = if relative_tolerance > ulp_tolerance { - relative_tolerance - } else { - ulp_tolerance - }; - - if !tolerance.is_finite() || tolerance <= K::Scalar::zero() { - tolerance = Self::duplicate_relative_tolerance(); - } - - tolerance - } - - /// Rebuilds the duplicate index when a scale-aware tolerance grows beyond - /// the current grid cell size, preserving complete candidate coverage. - fn ensure_duplicate_index_cell_size( - &self, - index: Option<&mut HashGridIndex>, - tolerance: K::Scalar, - ) { - let Some(index) = index else { - return; - }; - if !HashGridIndex::::supports_dimension() - || !tolerance.is_finite() - || tolerance <= K::Scalar::zero() - { - return; - } - if index.cell_size() >= tolerance { - return; - } - - let mut rebuilt = HashGridIndex::new(tolerance); - for (vkey, vertex) in self.tds.vertices() { - rebuilt.insert_vertex(vkey, vertex.point().coords()); - } - *index = rebuilt; - } - - /// Compares a squared distance against the duplicate tolerance without - /// overflowing the tolerance square on extreme coordinate scales. - fn duplicate_distance_within_tolerance(dist_sq: K::Scalar, tolerance: K::Scalar) -> bool { - let tolerance_sq = tolerance * tolerance; - if tolerance_sq.is_finite() { - dist_sq <= tolerance_sq - } else { - dist_sq.sqrt() <= tolerance - } - } - - /// Check for near-duplicate coordinates using the hash grid when available, with a - /// linear-scan fallback (O(n·D) per insertion) if the index is unavailable/unusable. - fn duplicate_coordinates_error( - &self, - coords: &[K::Scalar; D], - tolerance: K::Scalar, - index: Option<&HashGridIndex>, - ) -> Option { - let mut duplicate_found = false; - let make_duplicate_error = || { - let mut coordinates = String::from("["); - for (idx, coord) in coords.iter().enumerate() { - if idx != 0 { - coordinates.push_str(", "); - } - let _ = write!(&mut coordinates, "{coord:?}"); - } - coordinates.push(']'); - InsertionError::DuplicateCoordinates { coordinates } - }; - - if let Some(index) = index - && index.cell_size() >= tolerance - { - let mut candidate_count = 0usize; - let used_index = index.for_each_candidate_vertex_key(coords, |vkey| { - candidate_count = candidate_count.saturating_add(1); - let Some(vertex) = self.tds.vertex(vkey) else { - return true; - }; - - let vcoords = vertex.point().coords(); - let mut dist_sq = K::Scalar::zero(); - for i in 0..D { - let diff = vcoords[i] - coords[i]; - dist_sq += diff * diff; - } - - if Self::duplicate_distance_within_tolerance(dist_sq, tolerance) { - duplicate_found = true; - return false; - } - - true - }); - record_duplicate_detection_metrics(used_index, candidate_count, !used_index); - - if duplicate_found { - return Some(make_duplicate_error()); - } - - if used_index { - return None; - } - } else { - record_duplicate_detection_metrics(false, 0, true); - } - - for (_, existing_vertex) in self.tds.vertices() { - let existing_coords = existing_vertex.point().coords(); - let mut dist_sq = K::Scalar::zero(); - for i in 0..D { - let diff = coords[i] - existing_coords[i]; - dist_sq += diff * diff; - } - - if Self::duplicate_distance_within_tolerance(dist_sq, tolerance) { - duplicate_found = true; - break; - } - } - - if duplicate_found { - Some(make_duplicate_error()) - } else { - None - } - } - - /// Estimate a local length scale for perturbation based on nearby vertices. - /// - /// Uses the hint simplex when available; otherwise falls back to the closest - /// existing vertex. This keeps perturbations translation-invariant and - /// proportional to local feature size. - fn estimate_local_perturbation_scale( - &self, - coords: &[K::Scalar; D], - hint: Option, - ) -> K::Scalar { - let mut min_dist_sq: Option = None; - - let consider_vertex = |vertex: &Vertex, - min_dist_sq: &mut Option| { - let vcoords = vertex.point().coords(); - let mut dist_sq = K::Scalar::zero(); - for i in 0..D { - let diff = vcoords[i] - coords[i]; - dist_sq += diff * diff; - } - match min_dist_sq { - Some(current) => { - if dist_sq < *current { - *current = dist_sq; - } - } - None => { - *min_dist_sq = Some(dist_sq); - } - } - }; - - if let Some(simplex_key) = hint - && let Some(simplex) = self.tds.simplex(simplex_key) - { - for &vkey in simplex.vertices() { - if let Some(vertex) = self.tds.vertex(vkey) { - consider_vertex(vertex, &mut min_dist_sq); - } - } - } - - if min_dist_sq.is_none() { - for (_, vertex) in self.tds.vertices() { - consider_vertex(vertex, &mut min_dist_sq); - } - } - - let mut scale = min_dist_sq.map_or_else(K::Scalar::one, num_traits::Float::sqrt); - - let min_scale = K::Scalar::default_tolerance(); - if scale < min_scale { - scale = min_scale; - } - - scale - } - - // ------------------------------------------------------------------------- - // Topology safety net helpers - // ------------------------------------------------------------------------- - - /// Logs when Level 3 validation is triggered (debug builds only). - #[inline] - fn log_validation_trigger_if_enabled(&self, suspicion: SuspicionFlags) { - #[cfg(debug_assertions)] - if self.validation_policy.should_validate(suspicion) && suspicion.is_suspicious() { - tracing::debug!("Validation triggered by {suspicion:?}"); - } - - // Keep the parameter "used" in release builds where the debug-only logging - // is compiled out, so `cargo clippy -D warnings` stays clean across profiles. - #[cfg(not(debug_assertions))] - { - let _ = suspicion; - } - } - - /// Convert an [`InvariantError`] into the appropriate [`InsertionError`] variant. - /// - /// - `InvariantError::Tds(e)` → `InsertionError::TopologyValidation(e)` - /// - `InvariantError::Triangulation(e)` → `InsertionError::TopologyValidationFailed { source: e }` - /// - `InvariantError::Delaunay(e)` → `InsertionError::DelaunayValidationFailed { message }` - fn invariant_error_to_insertion_error(err: InvariantError) -> InsertionError { - match err { - InvariantError::Tds(tds_err) => InsertionError::TopologyValidation(tds_err), - InvariantError::Triangulation(tri_err) => InsertionError::TopologyValidationFailed { - message: "Topology validation failed".to_string(), - source: tri_err, - }, - InvariantError::Delaunay(dt_err) => { - InsertionError::DelaunayValidationFailed { source: dt_err } - } - } - } - - /// Runs mandatory link checks required by the topology guarantee. - fn validate_required_topology_links(&self) -> Result<(), InvariantError> { - if self.tds.number_of_simplices() == 0 { - return Ok(()); - } - - let facet_to_simplices: FacetToSimplicesMap = self.tds.build_facet_to_simplices_map()?; - validate_facet_degree(&facet_to_simplices)?; - validate_closed_boundary(&self.tds, &facet_to_simplices)?; - - if self.topology_guarantee.requires_ridge_links() { - validate_ridge_links(&self.tds)?; - } - - if self - .topology_guarantee - .requires_vertex_links_during_insertion() - { - validate_vertex_links(&self.tds, &facet_to_simplices)?; - } - - // Keep geometric orientation non-negotiable during incremental insertion, - // even when global validation is throttled. Run this after topology - // checks so topology diagnostics still surface first. - self.validate_geometric_simplex_orientation()?; - - Ok(()) - } - - /// Runs mandatory topology checks over the local simplices touched by insertion. - /// - /// Soundness boundary: the scoped path checks coherent orientation, local - /// pseudomanifold facet incidence, ridge links, and geometric simplex - /// orientation. Those local checks are sufficient only when `simplices` is - /// non-empty and `topology_guarantee` does not require vertex-link checks - /// during insertion; otherwise this explicitly falls back to - /// [`validate_required_topology_links`](Self::validate_required_topology_links). - /// See `REFERENCES.md`, "Scoped Local Validation and Flips" \[1\], for the - /// local-vs-global validation tradeoff and geometric conditioning context. - fn validate_required_topology_links_for_simplices( - &self, - simplices: &[SimplexKey], - ) -> Result<(), InvariantError> { - if self.tds.number_of_simplices() == 0 { - return Ok(()); - } - - if simplices.is_empty() - || self - .topology_guarantee - .requires_vertex_links_during_insertion() - { - return self.validate_required_topology_links(); - } - - self.tds - .validate_coherent_orientation_for_simplices(simplices)?; - validate_local_pseudomanifold_for_simplices(&self.tds, simplices)?; - - if self.topology_guarantee.requires_ridge_links() { - validate_ridge_links_for_simplices(&self.tds, simplices)?; - } - - self.validate_geometric_simplex_orientation_for_simplices(simplices)?; - - Ok(()) - } - - fn validation_after_insertion_work( - &self, - suspicion: SuspicionFlags, - ) -> Option { - if self.tds.number_of_simplices() == 0 { - return None; - } - - let should_validate = self.validation_policy.should_validate(suspicion); - let requires_required_topology_checks = self - .topology_guarantee - .requires_pseudomanifold_checks_during_insertion(); - - if should_validate { - Some(InsertionValidationWork::FullValidation) - } else if requires_required_topology_checks { - Some(InsertionValidationWork::RequiredTopologyLinks) - } else { - None - } - } - - fn validate_after_insertion_with_scope( - &self, - suspicion: SuspicionFlags, - local_simplices: Option<&[SimplexKey]>, - ) -> Result<(), InvariantError> { - let Some(work) = self.validation_after_insertion_work(suspicion) else { - return Ok(()); - }; - - self.log_validation_trigger_if_enabled(suspicion); - match work { - InsertionValidationWork::FullValidation => self.is_valid(), - InsertionValidationWork::RequiredTopologyLinks => local_simplices.map_or_else( - || self.validate_required_topology_links(), - |simplices| self.validate_required_topology_links_for_simplices(simplices), - ), - } - } - - /// Runs post-insertion validation and records count/timing telemetry for the selected work. - fn validate_after_insertion_and_record_telemetry( - &self, - suspicion: SuspicionFlags, - local_simplices: &[SimplexKey], - telemetry: &mut InsertionTelemetry, - telemetry_mode: InsertionTelemetryMode, - ) -> Result<(), InvariantError> { - let validation_work = self.validation_after_insertion_work(suspicion); - let validation_started = - validation_work.and_then(|_| Self::start_insertion_timing(telemetry_mode)); - let validation_result = - self.validate_after_insertion_with_scope(suspicion, Some(local_simplices)); - - if validation_work.is_some() { - Self::record_topology_validation_telemetry( - telemetry, - validation_started - .map(|started| Self::duration_nanos_saturating(started.elapsed())), - ); - } - - validation_result - } - - /// Repair neighbor pointers after local simplex removal without scanning the full TDS. - fn repair_neighbors_after_local_simplex_removal( - &mut self, - new_simplices: &SimplexKeyBuffer, - frontier_simplices: &[SimplexKey], - ) -> Result { - #[cfg(debug_assertions)] - tracing::debug!( - simplices = self.tds.number_of_simplices(), - surviving_new_simplex_seeds = new_simplices - .iter() - .filter(|&&simplex_key| self.tds.contains_simplex(simplex_key)) - .count(), - frontier_simplex_seeds = frontier_simplices - .iter() - .filter(|&&simplex_key| self.tds.contains_simplex(simplex_key)) - .count(), - "Before local neighbor-pointer repair" - ); - - if force_global_neighbor_rebuild_enabled() { - #[cfg(debug_assertions)] - tracing::debug!( - "DELAUNAY_FORCE_GLOBAL_NEIGHBOR_REBUILD set; using global neighbor rebuild" - ); - return repair_neighbor_pointers(&mut self.tds).map_err(|source| { - CavityFillingError::NeighborRebuild { - reason: source.into(), - } - .into() - }); - } - - #[cfg(debug_assertions)] - { - match repair_neighbor_pointers_local( - &mut self.tds, - new_simplices, - Some(frontier_simplices), - ) { - Ok(repaired) => Ok(repaired), - Err(local_error) => { - tracing::warn!( - error = %local_error, - "Local neighbor-pointer repair failed; falling back to global rebuild in debug mode" - ); - repair_neighbor_pointers(&mut self.tds).map_err(|source| { - CavityFillingError::NeighborRebuild { - reason: source.into(), - } - .into() - }) - } - } - } - - #[cfg(not(debug_assertions))] - { - repair_neighbor_pointers_local(&mut self.tds, new_simplices, Some(frontier_simplices)) - .map_err(|source| { - CavityFillingError::NeighborRebuild { - reason: source.into(), - } - .into() - }) - } - } - - /// Attempt an insertion, and if Level 3 validation fails, roll back and try a - /// conservative star-split fallback of the containing simplex. - #[expect( - clippy::too_many_arguments, - reason = "Topology safety net needs transactional rollback context plus telemetry mode" - )] - fn try_insert_with_topology_safety_net( - &mut self, - vertex: Vertex, - conflict_simplices: Option<&SimplexKeyBuffer>, - hint: Option, - attempt: usize, - tds_snapshot: &Tds, - telemetry: &mut InsertionTelemetry, - telemetry_mode: InsertionTelemetryMode, - ) -> Result { - let mut insert_ok = - self.try_insert_impl(vertex, conflict_simplices, hint, telemetry, telemetry_mode)?; - - if attempt > 0 { - insert_ok.suspicion.perturbation_used = true; - } - if insert_ok.suspicion.is_suspicious() { - insert_ok.delaunay_repair_required = true; - } - - // Skip Level 3 validation during bootstrap (vertices but no simplices yet). - if self.tds.number_of_simplices() == 0 { - return Ok(insert_ok); - } - - let validation_result = self.validate_after_insertion_and_record_telemetry( - insert_ok.suspicion, - &insert_ok.repair_seed_simplices, - telemetry, - telemetry_mode, - ); - if let Err(validation_err) = validation_result { - // Roll back to snapshot and attempt a star-split fallback for interior points. - self.tds = tds_snapshot.clone_for_rollback(); - return self.try_star_split_fallback_after_topology_failure( - vertex, - hint, - attempt, - validation_err, - telemetry, - telemetry_mode, - ); - } - - Ok(insert_ok) - } - - /// After a Level 3 topology validation failure, try to recover by performing a star-split - /// of the containing simplex (if the point can be re-located inside a simplex). - /// - /// Notes: - /// - This fallback is only applicable when the point re-locates to [`LocateResult::InsideSimplex`]. - /// - We re-run Level 3 validation after the fallback to avoid "recovering" into an invalid state. - fn try_star_split_fallback_after_topology_failure( - &mut self, - vertex: Vertex, - hint: Option, - attempt: usize, - validation_err: InvariantError, - telemetry: &mut InsertionTelemetry, - telemetry_mode: InsertionTelemetryMode, - ) -> Result { - let point = *vertex.point(); - let location = match locate_with_stats(&self.tds, &self.kernel, &point, hint) { - Ok((location, stats)) => { - Self::record_locate_telemetry(telemetry, location, &stats); - Ok(location) - } - Err(error) => Err(error), - }; - - let Ok(LocateResult::InsideSimplex(start_simplex)) = location else { - return Err(Self::invariant_error_to_insertion_error(validation_err)); - }; - - let mut star_conflict = SimplexKeyBuffer::new(); - star_conflict.push(start_simplex); - - match self.try_insert_impl( - vertex, - Some(&star_conflict), - Some(start_simplex), - telemetry, - telemetry_mode, - ) { - Ok(mut fallback_ok) => { - fallback_ok.suspicion.fallback_star_split = true; - if attempt > 0 { - fallback_ok.suspicion.perturbation_used = true; - } - fallback_ok.delaunay_repair_required = true; - - let validation_result = self.validate_after_insertion_and_record_telemetry( - fallback_ok.suspicion, - &fallback_ok.repair_seed_simplices, - telemetry, - telemetry_mode, - ); - if let Err(fallback_validation_err) = validation_result { - return Err(Self::invariant_error_to_insertion_error( - fallback_validation_err, - )); - } - - // Telemetry: the fallback succeeded, meaning we recovered from a topology - // validation failure without surfacing an insertion error to the caller. - TOPOLOGY_SAFETY_NET_STAR_SPLIT_FALLBACK_SUCCESSES.fetch_add(1, Ordering::Relaxed); - - #[cfg(debug_assertions)] - tracing::debug!( - "Topology safety-net: star-split fallback succeeded (start_simplex={start_simplex:?})" - ); - - Ok(fallback_ok) - } - Err(fallback_err) => Err(fallback_err), - } - } - - /// Ensure an interior insertion never proceeds with an empty conflict region. - /// - /// An empty conflict region would produce an empty cavity boundary, create no new simplices, and - /// leave the inserted vertex isolated (not incident to any simplex), which breaks Level 3 topology - /// validation via Euler characteristic. - fn ensure_non_empty_conflict_simplices( - conflict_simplices: Cow<'_, SimplexKeyBuffer>, - fallback_simplex: SimplexKey, - ) -> Cow<'_, SimplexKeyBuffer> { - if !conflict_simplices.is_empty() { - return conflict_simplices; - } - - if let Cow::Owned(mut owned) = conflict_simplices { - owned.push(fallback_simplex); - Cow::Owned(owned) - } else { - let mut owned = SimplexKeyBuffer::new(); - owned.push(fallback_simplex); - Cow::Owned(owned) - } - } - - /// Build the boundary facets for a "star-split" of the containing simplex. - fn star_split_boundary_facets(start_simplex: SimplexKey) -> CavityBoundaryBuffer { - (0..=D) - .map(|i| { - FacetHandle::new( - start_simplex, - u8::try_from(i).expect("facet index must fit in u8"), - ) - }) - .collect() - } - - /// Connectedness guard (localized). - /// - /// This check is designed to be **O(k·D)**, where `k` is the number of newly created simplices and - /// `D` is the triangulation dimension (each simplex has at most `D+1` neighbors). - /// - /// It validates two properties that are sufficient to catch the common “disconnected neighbor - /// graph after insertion” failure modes without walking the entire triangulation: - /// - /// 1. The surviving subset of `new_simplices` forms a single connected component (via neighbor pointers). - /// 2. If there are simplices outside that component, the new component is attached to at least one - /// existing simplex (via a *mutual* neighbor relationship). - fn validate_connectedness( - &self, - new_simplices: &SimplexKeyBuffer, - ) -> Result<(), InsertionError> { - let total_simplices = self.tds.number_of_simplices(); - if total_simplices == 0 { - return Ok(()); - } - - // Build a set of the *surviving* new simplices (some may have been removed during repair). - let mut new_set: SimplexKeySet = SimplexKeySet::default(); - new_set.reserve(new_simplices.len()); - for &ck in new_simplices { - if self.tds.contains_simplex(ck) { - new_set.insert(ck); - } - } - - if new_set.is_empty() { - return Err(InsertionError::TopologyValidation( - TdsError::InconsistentDataStructure { - message: "Disconnected triangulation detected after insertion: no surviving new simplices" - .to_string(), - }, - )); - } - - let expected_new_simplices = new_set.len(); - - let Some(&start) = new_set.iter().next() else { - return Err(InsertionError::TopologyValidation( - TdsError::InconsistentDataStructure { - message: - "new_set unexpectedly empty after non-empty check in validate_connectedness" - .to_string(), - }, - )); - }; - - let mut touches_existing_simplices = false; - - let visited = self.traverse_simplex_neighbor_graph( - start, - expected_new_simplices, - Some(&new_set), - |ck, nk| { - if touches_existing_simplices { - return; - } - - // For connectivity between new simplices and existing simplices, require *mutual* adjacency. - // This avoids treating one-way neighbor pointers as “connected”. - if let Some(neighbor_simplex) = self.tds.simplex(nk) - && neighbor_simplex - .neighbor_keys() - .is_some_and(|mut neighbor_keys| { - neighbor_keys.any(|neighbor| neighbor == Some(ck)) - }) - { - touches_existing_simplices = true; - } - }, - ); - - if visited.len() != expected_new_simplices { - return Err(InsertionError::TopologyValidation( - TdsError::InconsistentDataStructure { - message: format!( - "Disconnected triangulation detected after insertion: new-simplex subgraph visited {} of {} simplices", - visited.len(), - expected_new_simplices - ), - }, - )); - } - - // If there are simplices outside `new_set`, ensure the new component is attached to at least one - // of them (otherwise we'd be creating a disconnected component). - if total_simplices > expected_new_simplices && !touches_existing_simplices { - return Err(InsertionError::TopologyValidation( - TdsError::InconsistentDataStructure { - message: format!( - "Disconnected triangulation detected after insertion: new-simplex component ({expected_new_simplices} simplices) is not connected to existing simplices (total_simplices={total_simplices})" - ), - }, - )); - } - - Ok(()) - } - - /// Find all conflict simplices by scanning the entire triangulation. - /// - /// Test-only global conflict scanner for malformed-simplex error coverage. - /// - /// Exterior production insertion deliberately avoids this path: hull - /// extension is the local topological mutation, and Delaunay violations are - /// left to the cadenced or final repair layers. - #[cfg(test)] - fn find_conflict_region_global( - &self, - point: &Point, - ) -> Result { - #[cfg(debug_assertions)] - let log_enabled = std::env::var_os("DELAUNAY_DEBUG_HULL").is_some() - || std::env::var_os("DELAUNAY_DEBUG_CONFLICT").is_some(); - #[cfg(debug_assertions)] - let mut simplices_scanned = 0usize; - #[cfg(debug_assertions)] - let mut sign_positive = 0usize; - #[cfg(debug_assertions)] - let mut sign_zero = 0usize; - #[cfg(debug_assertions)] - let mut sign_negative = 0usize; - - let mut conflict_simplices = SimplexKeyBuffer::new(); - - for (simplex_key, simplex) in self.tds.simplices() { - #[cfg(debug_assertions)] - { - simplices_scanned = simplices_scanned.saturating_add(1); - } - // Collect simplex vertex points in canonical VertexKey order for consistent - // SoS perturbation priority. - let simplex_points = sorted_simplex_points(&self.tds, simplex).ok_or_else(|| { - ConflictError::SimplexDataAccessFailed { - simplex_key, - message: format!("Failed to resolve all {} simplex vertices", D + 1), - } - })?; - - if simplex_points.len() != D + 1 { - return Err(ConflictError::SimplexDataAccessFailed { - simplex_key, - message: format!("Expected {} vertices, got {}", D + 1, simplex_points.len()), - }); - } - - let sign = self.kernel.in_sphere(&simplex_points, point)?; - #[cfg(debug_assertions)] - { - if log_enabled { - tracing::debug!( - simplex_key = ?simplex_key, - sign, - "find_conflict_region_global: in_sphere sign" - ); - } - match sign.cmp(&0) { - CmpOrdering::Greater => { - sign_positive = sign_positive.saturating_add(1); - } - CmpOrdering::Equal => { - sign_zero = sign_zero.saturating_add(1); - } - CmpOrdering::Less => { - sign_negative = sign_negative.saturating_add(1); - } - } - } - if sign > 0 { - conflict_simplices.push(simplex_key); - } - } - - #[cfg(debug_assertions)] - if log_enabled { - tracing::debug!( - point = ?point, - simplices_scanned, - conflict_simplices = conflict_simplices.len(), - sign_positive, - sign_zero, - sign_negative, - "find_conflict_region_global: summary" - ); - } - - Ok(conflict_simplices) - } - - /// Returns true if any conflict simplex has a facet on the hull boundary. - #[cfg(test)] - fn conflict_region_touches_boundary( - &self, - conflict_simplices: &SimplexKeyBuffer, - ) -> Result { - if conflict_simplices.is_empty() { - return Ok(false); - } - - let facet_to_simplices = self - .tds - .build_facet_to_simplices_map() - .map_err(InsertionError::TopologyValidation)?; - - let mut boundary_facets: FastHashSet = - fast_hash_set_with_capacity(facet_to_simplices.len()); - for (facet_key, simplex_list) in &facet_to_simplices { - if simplex_list.len() == 1 { - boundary_facets.insert(*facet_key); - } - } - - if boundary_facets.is_empty() { - return Ok(false); - } - - for &simplex_key in conflict_simplices { - let simplex = self.tds.simplex(simplex_key).ok_or_else(|| { - InsertionError::TopologyValidation(TdsError::SimplexNotFound { - simplex_key, - context: "checking boundary facets for conflict region".to_string(), - }) - })?; - for facet_idx in 0..simplex.number_of_vertices() { - let mut facet_vertices: SmallBuffer = - SmallBuffer::with_capacity(D); - for (i, &vkey) in simplex.vertices().iter().enumerate() { - if i != facet_idx { - facet_vertices.push(vkey); - } - } - let facet_key = facet_key_from_vertices(&facet_vertices); - if boundary_facets.contains(&facet_key) { - return Ok(true); - } - } - } - - Ok(false) - } - - /// Perform cavity insertion given an explicit conflict region. - #[expect( - clippy::too_many_lines, - reason = "Keep cavity insertion and repair logic together for clarity" - )] - fn insert_with_conflict_region( - &mut self, - v_key: VertexKey, - point: &Point, - mut conflict_simplices: SimplexKeyBuffer, - fallback_simplex: Option, - suspicion: &mut SuspicionFlags, - ) -> Result { - #[cfg(not(debug_assertions))] - let _ = point; - - if conflict_simplices.is_empty() { - let Some(start_simplex) = fallback_simplex else { - return Err(CavityFillingError::EmptyConflictRegion { - fallback_simplex: None, - } - .into()); - }; - suspicion.empty_conflict_region = true; - suspicion.fallback_star_split = true; - conflict_simplices.push(start_simplex); - // The fallback star-split is topologically safe but not a full - // Bowyer-Watson conflict-region replacement, so local Delaunay - // repair must revisit it. - } - - // Preserve every simplex that participates in cavity shaping and is later - // removed from the final cavity so callers can seed local Delaunay - // repair from the surviving fringe. - let mut repair_seed_simplices = SimplexKeyBuffer::new(); - let mut delaunay_repair_required = suspicion.fallback_star_split; - - // Extract cavity boundary. - // - // Iteratively resolve cavity-boundary errors rather than immediately falling back to a - // star-split. Star-splits create non-Delaunay configurations that the global flip repair - // must fix; in high dimensions this is extremely slow. In all dimensions it is better - // to first attempt to reshape the conflict region: - // - // • RidgeFan – SHRINK: remove extra fan simplices (3rd, 4th, … facets). - // • DisconnectedBnd – EXPAND: add the non-conflict neighbors of the disconnected - // simplices to fill the topological "hole" in the conflict region - // that causes the disconnected boundary. Falls back to SHRINK - // if no non-conflict neighbors are found. - // • OpenBoundary – SHRINK: remove the simplex with the dangling facet. - // - // After each reshape we re-run extract_cavity_boundary. If the loop exhausts its - // budget without producing a valid boundary: - // • D>=3: return a retryable error so insert_transactional retries with a perturbed - // vertex instead of creating an un-repairable star-split. - // • D<3: fall through to the existing star-split fallback (the 2D flip repair - // guarantees convergence even from star-split configurations). - let mut boundary_facets = { - let mut extraction_result = extract_cavity_boundary(&self.tds, &conflict_simplices); - - { - const MAX_CAVITY_ITERATIONS: usize = 32; - let mut iterations: usize = 0; - let trace_enabled = cavity_reduction_trace_enabled(); - let mut trace_cavity_reduction = false; - let mut saw_ridge_fan_shrink = false; - - match &extraction_result { - Ok(boundary) => { - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || format!("initial_ok boundary_facets={}", boundary.len()), - ); - } - Err(err) => { - trace_cavity_reduction = trace_enabled - && !CAVITY_REDUCTION_TRACE_EMITTED.swap(true, Ordering::Relaxed); - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || format!("initial_err {}", cavity_conflict_error_summary(err)), - ); - } - } - - loop { - if iterations >= MAX_CAVITY_ITERATIONS { - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || "budget_exhausted".to_string(), - ); - break; - } - iterations += 1; - - match &extraction_result { - // RidgeFan: SHRINK – remove the simplices contributing extra boundary facets. - Err(ConflictError::RidgeFan { - extra_simplices, .. - }) if !extra_simplices.is_empty() && conflict_simplices.len() > D + 1 => { - #[cfg(debug_assertions)] - tracing::debug!( - remove_count = extra_simplices.len(), - conflict_simplices_before = conflict_simplices.len(), - "D={D}: cavity reduction (RidgeFan shrink)" - ); - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || format!("ridge_fan_shrink remove_simplices={extra_simplices:?}"), - ); - saw_ridge_fan_shrink = true; - delaunay_repair_required = true; - let remove_set: FastHashSet = - extra_simplices.iter().copied().collect(); - retain_simplices_and_record_removed( - &mut conflict_simplices, - &mut repair_seed_simplices, - |simplex_key| !remove_set.contains(&simplex_key), - ); - } - - // DisconnectedBoundary: EXPAND – add non-conflict neighbors of the - // disconnected simplices to fill the topological hole. These simplices form the - // "inner wall" of a donut-shaped conflict region; their non-conflict - // neighbors are the hole simplices that, when added, reconnect the boundary. - // Falls back to SHRINK if no non-conflict neighbors exist. - Err(ConflictError::DisconnectedBoundary { - disconnected_simplices, - .. - }) if !disconnected_simplices.is_empty() => { - let conflict_set: FastHashSet = - conflict_simplices.iter().copied().collect(); - let mut simplices_to_add: FastHashSet = - FastHashSet::default(); - if !saw_ridge_fan_shrink { - for &dc in disconnected_simplices { - if let Some(simplex) = self.tds.simplex(dc) - && let Some(neighbors) = simplex.neighbor_keys() - { - for neighbor_opt in neighbors { - if let Some(nk) = neighbor_opt - && !conflict_set.contains(&nk) - { - simplices_to_add.insert(nk); - } - } - } - } - } - - if !simplices_to_add.is_empty() { - // EXPAND: add the hole-filling simplices. - delaunay_repair_required = true; - #[cfg(debug_assertions)] - tracing::debug!( - add_count = simplices_to_add.len(), - conflict_simplices_before = conflict_simplices.len(), - "D={D}: cavity expansion (DisconnectedBoundary hole-fill)" - ); - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || { - let added: Vec = - simplices_to_add.iter().copied().collect(); - format!( - "disconnected_boundary_expand add_simplices={added:?}" - ) - }, - ); - for k in simplices_to_add { - conflict_simplices.push(k); - } - } else if conflict_simplices.len() > D + 1 { - // SHRINK fallback: no non-conflict neighbors found. - delaunay_repair_required = true; - #[cfg(debug_assertions)] - tracing::debug!( - remove_count = disconnected_simplices.len(), - conflict_simplices_before = conflict_simplices.len(), - "D={D}: cavity reduction (DisconnectedBoundary shrink fallback)" - ); - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || { - format!( - "disconnected_boundary_shrink remove_simplices={disconnected_simplices:?}" - ) - }, - ); - let remove_set: FastHashSet = - disconnected_simplices.iter().copied().collect(); - retain_simplices_and_record_removed( - &mut conflict_simplices, - &mut repair_seed_simplices, - |simplex_key| !remove_set.contains(&simplex_key), - ); - } else { - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || "disconnected_boundary_no_progress".to_string(), - ); - break; - } - } - - // OpenBoundary: SHRINK – remove the simplex with the dangling facet. - Err(ConflictError::OpenBoundary { open_simplex, .. }) - if conflict_simplices.len() > D + 1 => - { - delaunay_repair_required = true; - #[cfg(debug_assertions)] - tracing::debug!( - ?open_simplex, - conflict_simplices_before = conflict_simplices.len(), - "D={D}: cavity reduction (OpenBoundary shrink)" - ); - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || format!("open_boundary_shrink open_simplex={open_simplex:?}"), - ); - let open = *open_simplex; - retain_simplices_and_record_removed( - &mut conflict_simplices, - &mut repair_seed_simplices, - |simplex_key| simplex_key != open, - ); - } - - _ => { - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || "no_reduction_rule_matched".to_string(), - ); - break; - } - } - - extraction_result = extract_cavity_boundary(&self.tds, &conflict_simplices); - match &extraction_result { - Ok(boundary) => { - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || format!("reextract_ok boundary_facets={}", boundary.len()), - ); - } - Err(err) => { - log_cavity_reduction_event( - trace_cavity_reduction, - iterations, - &conflict_simplices, - || format!("reextract_err {}", cavity_conflict_error_summary(err)), - ); - } - } - } - } - - match extraction_result { - Ok(boundary) => boundary, - Err(err) => { - // For D=3 and D≥4: do NOT fall back to star-split once cavity reduction - // is exhausted. Star-splits create heavily non-Delaunay configurations - // (the star of the new vertex is only D+1 simplices instead of the correct - // conflict region) whose violations cannot be reliably fixed by the flip - // repair: isolated violations may exist in simplices that are not connected to - // the star-split star through any violation chain. Return a retryable - // error instead so insert_transactional can retry with a perturbed vertex - // and, after all retries, skip the vertex. A valid Delaunay triangulation - // with a few skipped vertices is preferable to an invalid one with all of - // them (the is_delaunay_property_only() check in build_with_shuffled_retries - // will reject the latter anyway). - // - // For D=2: star-split is used as a last resort. The 2D flip repair - // guarantees convergence from star-split configurations and the extra simplices - // are quickly handled by the k=2 repair loop. - let should_fallback = D < 3 - && matches!( - err, - ConflictError::NonManifoldFacet { .. } - | ConflictError::RidgeFan { .. } - | ConflictError::DisconnectedBoundary { .. } - | ConflictError::OpenBoundary { .. } - ); - - if should_fallback { - let Some(start_simplex) = fallback_simplex else { - return Err(err.into()); - }; - - suspicion.fallback_star_split = true; - delaunay_repair_required = true; - - #[cfg(debug_assertions)] - tracing::warn!( - "Conflict region degeneracy ({err}); falling back to star-split of simplex {start_simplex:?}" - ); - - let mut replacement = SimplexKeyBuffer::new(); - replacement.push(start_simplex); - replace_simplices_and_record_removed( - &mut conflict_simplices, - &mut repair_seed_simplices, - replacement, - ); - - Self::star_split_boundary_facets(start_simplex) - } else { - #[cfg(debug_assertions)] - tracing::debug!( - "D={D}: cavity boundary unresolvable ({err}); returning retryable error" - ); - return Err(err.into()); - } - } - } - }; - - // Fallback: never allow an insertion to create a dangling vertex. - if boundary_facets.is_empty() { - let Some(start_simplex) = fallback_simplex else { - return Err(CavityFillingError::EmptyBoundary { - fallback_simplex: None, - } - .into()); - }; - - suspicion.empty_conflict_region = true; - suspicion.fallback_star_split = true; - delaunay_repair_required = true; - - #[cfg(debug_assertions)] - tracing::warn!( - "Empty cavity boundary; falling back to splitting containing simplex {start_simplex:?}" - ); - - let mut replacement = SimplexKeyBuffer::new(); - replacement.push(start_simplex); - replace_simplices_and_record_removed( - &mut conflict_simplices, - &mut repair_seed_simplices, - replacement, - ); - boundary_facets = Self::star_split_boundary_facets(start_simplex); - } - - // Fill cavity BEFORE removing old simplices. - let new_simplices = - fill_cavity_replacing_simplices(&mut self.tds, v_key, &boundary_facets)?; - self.canonicalize_positive_orientation_for_simplices(&new_simplices)?; - - // Post-insertion orientation audit: verify that canonicalization - // actually produced all-positive orientations among the new simplices. - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_ORIENTATION").is_some() { - let mut pos = 0_usize; - let mut neg = 0_usize; - let mut deg = 0_usize; - let mut fail = 0_usize; - for &ck in &new_simplices { - if let Some(c) = self.tds.simplex(ck) { - match self.evaluate_simplex_orientation_for_context( - ck, - c, - "post-insertion orientation audit", - "orientation predicate failed during post-insertion audit", - ) { - Ok(o) if o > 0 => pos += 1, - Ok(o) if o < 0 => neg += 1, - Ok(_) => deg += 1, - Err(ref e) => { - fail += 1; - tracing::warn!( - simplex_key = ?ck, - error = %e, - "post-insertion orientation audit: evaluation failed" - ); - } - } - } - } - if neg > 0 || fail > 0 { - tracing::warn!( - new_simplices = new_simplices.len(), - positive = pos, - negative = neg, - degenerate = deg, - eval_errors = fail, - "post-insertion orientation audit: NEGATIVE simplices or evaluation errors after canonicalization" - ); - } else { - tracing::debug!( - new_simplices = new_simplices.len(), - positive = pos, - degenerate = deg, - "post-insertion orientation audit: all simplices positive" - ); - } - } - - // Wire neighbors (while both old and new simplices exist) - let external_facets = - external_facets_for_boundary(&self.tds, &conflict_simplices, &boundary_facets)?; - wire_cavity_neighbors( - &mut self.tds, - &new_simplices, - external_facets.iter().copied(), - Some(&conflict_simplices), - )?; - - // Drop any repair-seed entries that were removed earlier but later got - // reintroduced into the final conflict region. Those keys will be - // deleted by `remove_simplices_by_keys` below, so they cannot seed repair. - let dead_conflict_simplices: FastHashSet = - conflict_simplices.iter().copied().collect(); - repair_seed_simplices.retain(|ck| !dead_conflict_simplices.contains(ck)); - let mut seen_repair_seed_simplices = FastHashSet::default(); - repair_seed_simplices.retain(|ck| seen_repair_seed_simplices.insert(*ck)); - - // Remove conflict simplices (now that new simplices are wired up) - let _removed_count = self.tds.remove_simplices_by_keys(&conflict_simplices); - - // Iteratively repair non-manifold topology until facet sharing is valid - let mut total_removed = 0; - let mut facet_sharing_known_valid = true; - let mut neighbor_repair_frontier = SimplexKeyBuffer::new(); - #[cfg_attr( - not(debug_assertions), - expect( - unused_variables, - reason = "`iteration` is only used for debug logging", - ) - )] - for iteration in 0..MAX_REPAIR_ITERATIONS { - // Check for non-manifold issues in newly created simplices (local scan) - let simplices_to_check: SimplexKeyBuffer = new_simplices - .iter() - .copied() - .filter(|ck| self.tds.contains_simplex(*ck)) - .collect(); - - if let Some(issues) = self.detect_local_facet_issues(&simplices_to_check)? { - // Only mark this as "suspicious" if we *actually* detected local facet issues - // and entered the repair path. - suspicion.repair_loop_entered = true; - delaunay_repair_required = true; - - #[cfg(debug_assertions)] - tracing::debug!( - "Repair iteration {}: {} over-shared facets detected, removing simplices...", - iteration + 1, - issues.len() - ); - - let repair = self.repair_local_facet_issues_with_frontier(&issues)?; - let removed = repair.removed_count; - - // Early exit if repair made no progress - if removed == 0 { - #[cfg(debug_assertions)] - tracing::warn!( - "No simplices removed in iteration {} - repair cannot make progress", - iteration + 1 - ); - return Err(InsertionError::TopologyValidation( - TdsError::InconsistentDataStructure { - message: format!( - "Repair stalled: {} over-shared facets remain but no simplices could be removed", - issues.len() - ), - }, - )); - } - - total_removed += removed; - neighbor_repair_frontier.extend(repair.frontier_simplices); - - if removed > 0 { - suspicion.simplices_removed = true; - delaunay_repair_required = true; - } - - #[cfg(debug_assertions)] - tracing::debug!( - removed_simplices = ?repair.removed_simplices, - "Removed {removed} simplices (total: {total_removed})" - ); - - // Early exit if repair succeeded - facet_sharing_known_valid = self.tds.validate_facet_sharing().is_ok(); - if facet_sharing_known_valid { - break; - } - } else { - // No more non-manifold issues - safe to rebuild neighbors - break; - } - } - - // Rebuild neighbor pointers now that topology is manifold. - #[cfg(debug_assertions)] - tracing::debug!("After repair loop: total_removed={total_removed}"); - - if !facet_sharing_known_valid { - return Err(CavityFillingError::InvalidFacetSharingAfterRepair { - stage: CavityRepairStage::PrimaryInsertion, - } - .into()); - } - - // Global neighbor rebuild is expensive. In the common case (no simplices removed during the - // local facet-repair loop), `wire_cavity_neighbors` has already glued the cavity locally. - // - // If we *did* remove simplices during the repair loop, repair only the new-simplex/frontier - // neighborhood unless the force-rebuild diagnostic environment variable is set. - if total_removed > 0 { - let repaired = self.repair_neighbors_after_local_simplex_removal( - &new_simplices, - &neighbor_repair_frontier, - )?; - suspicion.neighbor_pointers_rebuilt = repaired > 0; - delaunay_repair_required = true; - } - - // New cavity simplices were canonicalized on creation; validate the local - // orientation frontier without scanning the whole triangulation. - let mut orientation_simplices = SimplexKeyBuffer::new(); - append_live_unique_simplex_seeds(&self.tds, &new_simplices, &mut orientation_simplices); - append_live_unique_simplex_seeds( - &self.tds, - &neighbor_repair_frontier, - &mut orientation_simplices, - ); - self.validate_local_orientation_for_simplices(&orientation_simplices)?; - - // Assign an incident simplex for the inserted vertex without a global rebuild. - let hint = new_simplices.iter().copied().find(|&ck| { - self.tds - .simplex(ck) - .is_some_and(|simplex| simplex.contains_vertex(v_key)) - }); - if let Some(incident_simplex) = hint - && let Some(vertex) = self.tds.vertex_mut(v_key) - { - vertex.set_incident_simplex(Some(incident_simplex)); - } - - // Optional debug: validate neighbor pointers by forcing a full facet walk (no hint). - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_VALIDATE_LOCATE").is_some() { - let _ = locate(&self.tds, &self.kernel, point, None)?; - } - - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_RIDGE_LINK").is_some() { - match validate_ridge_links(&self.tds) { - Ok(()) => { - tracing::debug!( - "insert_with_conflict_region: ridge-link validation passed after insertion" - ); - } - Err(err) => { - tracing::warn!( - error = ?err, - "insert_with_conflict_region: ridge-link validation failed after insertion" - ); - } - } - } - - // Repair stale incident-simplex pointers (e.g. pointing to deleted conflict-region - // simplices) and error only for truly isolated vertices (in zero simplices). - self.repair_stale_incident_simplices()?; - - // Connectedness guard (STRUCTURAL SAFETY, NOT Level 3 validation) - self.validate_connectedness(&new_simplices)?; - - // Seed follow-up Delaunay repair from the local insertion product. Higher layers - // use these simplices to avoid rediscovering the inserted vertex star with a global scan. - append_live_unique_simplex_seeds(&self.tds, &new_simplices, &mut repair_seed_simplices); - append_live_unique_simplex_seeds( - &self.tds, - &neighbor_repair_frontier, - &mut repair_seed_simplices, - ); - - // Return hint for next insertion - Ok(CavityInsertionOutcome { - hint, - simplices_removed: total_removed, - repair_seed_simplices, - delaunay_repair_required: delaunay_repair_required || suspicion.is_suspicious(), - }) - } - - /// Repair stale incident-simplex pointers and detect truly isolated vertices. - /// - /// After cavity filling and simplex removal, pre-existing boundary vertices may - /// still reference deleted conflict-region simplices via a stale `incident_simplex`. - /// For each vertex with a stale or missing `incident_simplex`, this scans all - /// simplices for a valid one and updates the pointer. Returns an error only if a - /// vertex is in zero simplices (truly isolated). - fn repair_stale_incident_simplices(&mut self) -> Result<(), InsertionError> { - let stale_vertices: Vec<_> = { - let tds = &self.tds; - tds.vertices() - .filter(|(vk, v)| { - !v.incident_simplex().is_some_and(|simplex_key| { - tds.simplex(simplex_key) - .is_some_and(|simplex| simplex.contains_vertex(*vk)) - }) - }) - .map(|(vk, v)| (vk, v.uuid())) - .collect() - }; - #[cfg(debug_assertions)] - if !stale_vertices.is_empty() && std::env::var_os("DELAUNAY_DEBUG_HULL").is_some() { - tracing::debug!( - stale_count = stale_vertices.len(), - "repairing stale incident-simplex pointers" - ); - } - for &(vk, uuid) in &stale_vertices { - let repaired_simplex = self - .tds - .simplices() - .find_map(|(ck, simplex)| simplex.contains_vertex(vk).then_some(ck)); - if let Some(simplex_key) = repaired_simplex { - if let Some(vertex) = self.tds.vertex_mut(vk) { - vertex.set_incident_simplex(Some(simplex_key)); - } - } else { - // Truly isolated: no simplex in the TDS contains this vertex. - return Err(InsertionError::TopologyValidationFailed { - message: "Truly isolated vertex detected during stale incident-simplex repair" - .to_string(), - source: TriangulationValidationError::IsolatedVertex { - vertex_key: vk, - vertex_uuid: uuid, - }, - }); - } - } - Ok(()) - } - - /// Records one point-location result into insertion telemetry. - #[inline] - fn record_locate_telemetry( - telemetry: &mut InsertionTelemetry, - location: LocateResult, - stats: &LocateStats, - ) { - telemetry.locate_calls = telemetry.locate_calls.saturating_add(1); - telemetry.locate_walk_steps_total = telemetry - .locate_walk_steps_total - .saturating_add(stats.walk_steps); - telemetry.locate_walk_steps_max = telemetry.locate_walk_steps_max.max(stats.walk_steps); - - if stats.used_hint { - telemetry.locate_hint_uses = telemetry.locate_hint_uses.saturating_add(1); - } - - if stats.fell_back_to_scan() { - telemetry.locate_scan_fallbacks = telemetry.locate_scan_fallbacks.saturating_add(1); - } - - match location { - LocateResult::InsideSimplex(_) => { - telemetry.located_inside = telemetry.located_inside.saturating_add(1); - } - LocateResult::Outside => { - telemetry.located_outside = telemetry.located_outside.saturating_add(1); - } - LocateResult::OnFacet(_, _) | LocateResult::OnEdge(_) | LocateResult::OnVertex(_) => { - telemetry.located_on_boundary = telemetry.located_on_boundary.saturating_add(1); - } - } - } - - /// Records conflict-region size counters without touching timing fields. - #[inline] - fn record_conflict_region_telemetry(telemetry: &mut InsertionTelemetry, simplices: usize) { - telemetry.conflict_region_calls = telemetry.conflict_region_calls.saturating_add(1); - telemetry.conflict_region_simplices_total = telemetry - .conflict_region_simplices_total - .saturating_add(simplices); - telemetry.conflict_region_simplices_max = - telemetry.conflict_region_simplices_max.max(simplices); - } - - /// Records measured conflict-region time when timing telemetry is enabled. - #[inline] - fn record_conflict_region_timing(telemetry: &mut InsertionTelemetry, elapsed_nanos: u64) { - telemetry.conflict_region_nanos = telemetry - .conflict_region_nanos - .saturating_add(elapsed_nanos); - telemetry.conflict_region_nanos_max = - telemetry.conflict_region_nanos_max.max(elapsed_nanos); - } - - /// Records one cavity insertion attempt and its optional elapsed time. - #[inline] - fn record_cavity_insertion_telemetry( - telemetry: &mut InsertionTelemetry, - elapsed_nanos: Option, - ) { - telemetry.cavity_insertion_calls = telemetry.cavity_insertion_calls.saturating_add(1); - if let Some(elapsed_nanos) = elapsed_nanos { - telemetry.cavity_insertion_nanos = telemetry - .cavity_insertion_nanos - .saturating_add(elapsed_nanos); - telemetry.cavity_insertion_nanos_max = - telemetry.cavity_insertion_nanos_max.max(elapsed_nanos); - } - } - - /// Records one hull-extension attempt and its optional elapsed time. - #[inline] - fn record_hull_extension_telemetry( - telemetry: &mut InsertionTelemetry, - elapsed_nanos: Option, - ) { - telemetry.hull_extension_calls = telemetry.hull_extension_calls.saturating_add(1); - if let Some(elapsed_nanos) = elapsed_nanos { - telemetry.hull_extension_nanos = - telemetry.hull_extension_nanos.saturating_add(elapsed_nanos); - telemetry.hull_extension_nanos_max = - telemetry.hull_extension_nanos_max.max(elapsed_nanos); - } - } - - /// Records one topology-validation pass and its optional elapsed time. - #[inline] - fn record_topology_validation_telemetry( - telemetry: &mut InsertionTelemetry, - elapsed_nanos: Option, - ) { - telemetry.topology_validation_calls = telemetry.topology_validation_calls.saturating_add(1); - if let Some(elapsed_nanos) = elapsed_nanos { - telemetry.topology_validation_nanos = telemetry - .topology_validation_nanos - .saturating_add(elapsed_nanos); - telemetry.topology_validation_nanos_max = - telemetry.topology_validation_nanos_max.max(elapsed_nanos); - } - } - - /// Convert a duration to nanoseconds while saturating at `u64::MAX`. - #[inline] - fn duration_nanos_saturating(duration: Duration) -> u64 { - u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX) - } - - /// Starts a wall-clock timer only when insertion telemetry will publish timings. - #[inline] - fn start_insertion_timing(telemetry_mode: InsertionTelemetryMode) -> Option { - telemetry_mode.records_timings().then(Instant::now) - } - - fn collect_exterior_repair_seed_simplices( - &self, - point: &Point, - terminal_simplex: SimplexKey, - locate_stats: &LocateStats, - telemetry_mode: InsertionTelemetryMode, - telemetry: &mut InsertionTelemetry, - ) -> Result { - if locate_stats.fell_back_to_scan() || !self.tds.contains_simplex(terminal_simplex) { - return Ok(SimplexKeyBuffer::new()); - } - - let conflict_started = Self::start_insertion_timing(telemetry_mode); - let local_seed_simplices = collect_local_exterior_conflict_seed_simplices( - &self.tds, - &self.kernel, - point, - terminal_simplex, - )?; - Self::record_conflict_region_telemetry( - telemetry, - local_seed_simplices.conflict_simplices_found, - ); - if let Some(conflict_started) = conflict_started { - Self::record_conflict_region_timing( - telemetry, - Self::duration_nanos_saturating(conflict_started.elapsed()), - ); - } - Ok(local_seed_simplices.seed_simplices) - } - - /// Internal implementation of insert without retry logic. - /// Returns the result and the number of simplices removed during repair. - /// - /// Note: `conflict_simplices` parameter is optional. If `None`, it will be computed automatically - /// for interior points using `locate()` + `find_conflict_region()`. - #[expect( - clippy::too_many_lines, - reason = "Complex insertion logic; splitting further would harm readability" - )] - fn try_insert_impl( - &mut self, - vertex: Vertex, - conflict_simplices: Option<&SimplexKeyBuffer>, - hint: Option, - telemetry: &mut InsertionTelemetry, - telemetry_mode: InsertionTelemetryMode, - ) -> Result { - let mut suspicion = SuspicionFlags::default(); - - // CRITICAL: Capture UUID and point BEFORE inserting into TDS - // Rationale: - // - inserted_uuid: Needed to remap v_key after TDS rebuild (lines 736-744) - // when building initial simplex. The rebuild replaces self.tds entirely, - // invalidating all previous VertexKeys. - // - point: Needed for locate(), find_conflict_region(), and extend_hull() calls - // (lines 752, 760, 879, 895). After TDS rebuild, we cannot access the vertex - // via the old v_key, so we must have the point value captured. - let inserted_uuid = vertex.uuid(); - let point = *vertex.point(); - - vertex.is_valid().map_err(|source| { - InsertionError::TopologyValidation(TdsError::InvalidVertex { - vertex_id: inserted_uuid, - source, - }) - })?; - - // 1. Insert vertex into Tds - let mut v_key = self - .tds - .insert_vertex_with_mapping(vertex) - .map_err(InsertionError::from)?; - - // 2. Check if we need to bootstrap the initial simplex - let num_vertices = self.tds.number_of_vertices(); - - if num_vertices < D + 1 { - // Bootstrap phase: just accumulate vertices, no simplices yet - return Ok(TryInsertImplOk { - inserted: (v_key, None), - simplices_removed: 0, - suspicion, - repair_seed_simplices: SimplexKeyBuffer::new(), - delaunay_repair_required: false, - }); - } else if num_vertices == D + 1 { - // Build initial simplex from all D+1 vertices - let all_vertices: Vec<_> = self.tds.vertices().map(|(_, v)| *v).collect(); - let new_tds = Self::build_initial_simplex(&all_vertices).map_err(|source| { - CavityFillingError::InitialSimplexConstruction { - reason: source.into(), - } - })?; - - // Replace empty TDS with simplex TDS (preserve kernel) - self.tds = new_tds; - - // Re-map vertex key to the rebuilt TDS - v_key = self.tds.vertex_key_from_uuid(&inserted_uuid).ok_or( - CavityFillingError::RebuiltVertexMissing { - uuid: inserted_uuid, - }, - )?; - - // Return first simplex key for hint caching - let first_simplex = self.tds.simplex_keys().next(); - return Ok(TryInsertImplOk { - inserted: (v_key, first_simplex), - simplices_removed: 0, - suspicion, - repair_seed_simplices: SimplexKeyBuffer::new(), - delaunay_repair_required: false, - }); - } - - // 3. Locate containing simplex (for vertex D+2 and beyond). - // - // `locate()` delegates to `locate_with_stats()`, so collecting the stats here keeps - // the same point-location algorithm while making release-mode batch diagnostics useful. - let locate_trace = locate_with_trace(&self.tds, &self.kernel, &point, hint)?; - let location = locate_trace.result; - let locate_stats = locate_trace.stats; - Self::record_locate_telemetry(telemetry, location, &locate_stats); - - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_HULL").is_some() - || std::env::var_os("DELAUNAY_DEBUG_LOCATE").is_some() - { - tracing::debug!( - point = ?point, - location = ?location, - start_simplex = ?locate_stats.start_simplex, - used_hint = locate_stats.used_hint, - walk_steps = locate_stats.walk_steps, - fallback = ?locate_stats.fallback, - "try_insert_impl: locate stats" - ); - } - - // 4. Determine the supported insertion site and any conflict simplices it needs. - let insertion_site = match (location, conflict_simplices) { - (LocateResult::InsideSimplex(start_simplex), None) => { - // Interior point: compute conflict region automatically. - // - // IMPORTANT: - // `find_conflict_region()` (Bowyer–Watson style) can legitimately return an empty - // set when the point lies inside the triangulation but is not strictly inside any - // existing simplex circumsphere (e.g., obtuse tetrahedra whose circumsphere does not - // contain all interior points). - // - // An empty conflict region would produce an empty cavity boundary, create no new - // simplices, and leave the inserted vertex isolated (not incident to any simplex), which - // breaks Level 3 topology validation via Euler characteristic. - // - // Fallback: treat the containing simplex as the conflict region, effectively performing - // a star-split of that simplex to keep the simplicial complex connected. - let conflict_started = Self::start_insertion_timing(telemetry_mode); - let computed = - find_conflict_region(&self.tds, &self.kernel, &point, start_simplex)?; - Self::record_conflict_region_telemetry(telemetry, computed.len()); - if let Some(conflict_started) = conflict_started { - Self::record_conflict_region_timing( - telemetry, - Self::duration_nanos_saturating(conflict_started.elapsed()), - ); - } - - #[cfg(feature = "diagnostics")] - if std::env::var_os("DELAUNAY_DEBUG_CONFLICT_VERIFY").is_some() { - let missed = verify_conflict_region_completeness( - &self.tds, - &self.kernel, - &point, - &computed, - ); - if missed > 0 { - tracing::warn!( - missed, - bfs_conflict = computed.len(), - start_simplex = ?start_simplex, - point = ?point, - num_vertices = self.tds.number_of_vertices(), - num_simplices = self.tds.number_of_simplices(), - "try_insert_impl: INCOMPLETE conflict region at insertion" - ); - } - } - - if computed.is_empty() { - suspicion.empty_conflict_region = true; - suspicion.fallback_star_split = true; - } - InsertionSite::Interior { - start_simplex, - conflict_simplices: Self::ensure_non_empty_conflict_simplices( - Cow::Owned(computed), - start_simplex, - ), - } - } - (LocateResult::InsideSimplex(start_simplex), Some(simplices)) => { - // If the caller provided an empty conflict region (can happen if the Delaunay layer - // computes conflicts using a strict in-sphere test), we must still replace at least - // one simplex; otherwise we'd create no cavity, no new simplices, and leave a dangling - // vertex (χ increases by 1, typically showing up as χ=2 for Ball(3)). - if simplices.is_empty() { - suspicion.empty_conflict_region = true; - suspicion.fallback_star_split = true; - } - InsertionSite::Interior { - start_simplex, - conflict_simplices: Self::ensure_non_empty_conflict_simplices( - Cow::Borrowed(simplices), - start_simplex, - ), - } - } - (LocateResult::Outside, None) => { - // Exterior insertion is the hull-extension case. Avoid the old - // full-TDS conflict scan here; it was O(number_of_simplices) per - // exterior point, often only to rediscover that the hull path - // was required anyway. Cadenced and final Delaunay repair own - // any local empty-circumsphere cleanup after the hull mutation. - #[cfg(debug_assertions)] - if env::var_os("DELAUNAY_DEBUG_HULL").is_some() { - tracing::debug!( - "Outside insertion: skipping global conflict-region scan; using hull extension" - ); - } - let repair_seed_simplices = self.collect_exterior_repair_seed_simplices( - &point, - locate_trace.terminal_simplex, - &locate_stats, - telemetry_mode, - telemetry, - )?; - InsertionSite::Exterior { - conflict_simplices: None, - repair_seed_simplices, - } - } - (LocateResult::Outside, Some(simplices)) => { - if simplices.is_empty() { - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_HULL").is_some() { - tracing::debug!( - "Outside insertion: caller provided empty conflict region; will use hull extension" - ); - } - let repair_seed_simplices = self.collect_exterior_repair_seed_simplices( - &point, - locate_trace.terminal_simplex, - &locate_stats, - telemetry_mode, - telemetry, - )?; - InsertionSite::Exterior { - conflict_simplices: None, - repair_seed_simplices, - } - } else { - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_HULL").is_some() { - tracing::debug!( - conflict_simplices = simplices.len(), - "Outside insertion: using caller-provided conflict region" - ); - } - InsertionSite::Exterior { - conflict_simplices: Some(Cow::Borrowed(simplices)), - repair_seed_simplices: simplices.iter().copied().collect(), - } - } - } - (location, _) => { - // Degenerate locations (OnFacet, OnEdge, OnVertex) - return Err(CavityFillingError::UnsupportedDegenerateLocation { location }.into()); - } - }; - - // 5. Handle different insertion sites. - match insertion_site { - InsertionSite::Interior { - start_simplex, - conflict_simplices, - } => { - let conflict_simplices = conflict_simplices.into_owned(); - let cavity_started = Self::start_insertion_timing(telemetry_mode); - let insertion_result = self.insert_with_conflict_region( - v_key, - &point, - conflict_simplices, - Some(start_simplex), - &mut suspicion, - ); - Self::record_cavity_insertion_telemetry( - telemetry, - cavity_started - .map(|started| Self::duration_nanos_saturating(started.elapsed())), - ); - let outcome = insertion_result?; - Ok(TryInsertImplOk { - inserted: (v_key, outcome.hint), - simplices_removed: outcome.simplices_removed, - suspicion, - repair_seed_simplices: outcome.repair_seed_simplices, - delaunay_repair_required: outcome.delaunay_repair_required, - }) - } - InsertionSite::Exterior { - conflict_simplices, - repair_seed_simplices: exterior_repair_seed_simplices, - } => { - if let Some(conflict_simplices) = conflict_simplices { - let conflict_simplices = conflict_simplices.into_owned(); - #[cfg(debug_assertions)] - let conflict_len = conflict_simplices.len(); - #[cfg(debug_assertions)] - tracing::debug!( - "Outside insertion attempting cavity insertion with conflict region size {conflict_len}" - ); - let cavity_started = Self::start_insertion_timing(telemetry_mode); - let result = self.insert_with_conflict_region( - v_key, - &point, - conflict_simplices, - None, - &mut suspicion, - ); - Self::record_cavity_insertion_telemetry( - telemetry, - cavity_started - .map(|started| Self::duration_nanos_saturating(started.elapsed())), - ); - match result { - Ok(outcome) => { - return Ok(TryInsertImplOk { - inserted: (v_key, outcome.hint), - simplices_removed: outcome.simplices_removed, - suspicion, - repair_seed_simplices: outcome.repair_seed_simplices, - delaunay_repair_required: true, - }); - } - Err(err) => { - // For exterior points, a "global" conflict region can intersect the hull, - // producing an open/disconnected cavity boundary. In these cases we fall back - // to hull extension instead of surfacing an insertion error. - // - // IMPORTANT: Only ConflictError variants are safe to fall back from here. - // These originate from `extract_cavity_boundary` which runs BEFORE any TDS - // mutation. Errors like `IsolatedVertex` originate from AFTER the cavity - // has been filled, neighbors wired, and conflict simplices removed — the TDS - // is already heavily mutated and hull extension on that state is unsound. - let should_fallback = matches!( - &err, - InsertionError::ConflictRegion( - ConflictError::NonManifoldFacet { .. } - | ConflictError::RidgeFan { .. } - | ConflictError::DisconnectedBoundary { .. } - | ConflictError::OpenBoundary { .. } - ) - ); - - if should_fallback { - #[cfg(debug_assertions)] - tracing::warn!( - "Outside insertion conflict boundary degeneracy ({err}) (conflict_simplices={conflict_len}); falling back to hull extension" - ); - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_HULL").is_some() { - tracing::debug!( - error = %err, - "Outside insertion: cavity insertion failed; using hull extension" - ); - } - } else { - #[cfg(debug_assertions)] - tracing::warn!("Outside insertion cavity insertion failed: {err}"); - return Err(err); - } - } - } - } - // Exterior vertex: extend convex hull - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_HULL").is_some() { - tracing::debug!( - point = ?point, - "Outside insertion: proceeding to hull extension" - ); - } - let hull_started = Self::start_insertion_timing(telemetry_mode); - let hull_result = extend_hull(&mut self.tds, &self.kernel, v_key, &point); - Self::record_hull_extension_telemetry( - telemetry, - hull_started.map(|started| Self::duration_nanos_saturating(started.elapsed())), - ); - let new_simplices = match hull_result { - Ok(simplices) => simplices, - Err(err) => { - let retry_inside = matches!( - &err, - InsertionError::HullExtension { - reason: HullExtensionReason::NoVisibleFacets - } - ); - if retry_inside { - let fallback_location = - locate_by_scan(&self.tds, &self.kernel, &point)?; - // This retry starts as a scan, so account for the fallback - // explicitly and let the common recorder handle the outcome. - telemetry.locate_scan_fallbacks = - telemetry.locate_scan_fallbacks.saturating_add(1); - let scan_start_simplex = self - .tds - .simplex_keys() - .next() - .ok_or(LocateError::EmptyTriangulation)?; - let scan_stats = LocateStats { - start_simplex: scan_start_simplex, - used_hint: false, - walk_steps: 0, - fallback: None, - }; - Self::record_locate_telemetry( - telemetry, - fallback_location, - &scan_stats, - ); - if let LocateResult::InsideSimplex(start_simplex) = fallback_location { - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_HULL").is_some() { - tracing::warn!( - point = ?point, - start_simplex = ?start_simplex, - "Outside insertion: no visible facets; retrying as interior with star-split" - ); - } - suspicion.fallback_star_split = true; - let mut star_conflict = SimplexKeyBuffer::new(); - star_conflict.push(start_simplex); - let cavity_started = Self::start_insertion_timing(telemetry_mode); - let insertion_result = self.insert_with_conflict_region( - v_key, - &point, - star_conflict, - Some(start_simplex), - &mut suspicion, - ); - Self::record_cavity_insertion_telemetry( - telemetry, - cavity_started.map(|started| { - Self::duration_nanos_saturating(started.elapsed()) - }), - ); - let outcome = insertion_result?; - return Ok(TryInsertImplOk { - inserted: (v_key, outcome.hint), - simplices_removed: outcome.simplices_removed, - suspicion, - repair_seed_simplices: outcome.repair_seed_simplices, - delaunay_repair_required: true, - }); - } - } - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_HULL").is_some() { - tracing::warn!( - point = ?point, - error = %err, - "Outside insertion: hull extension failed" - ); - } - return Err(err); - } - }; - self.canonicalize_positive_orientation_for_simplices(&new_simplices)?; - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_HULL").is_some() { - tracing::debug!( - new_simplices = new_simplices.len(), - "Outside insertion: hull extension succeeded" - ); - } - - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_NEIGHBORS").is_some() { - let mut total_slots = 0usize; - let mut neighbor_none = 0usize; - let mut neighbor_missing = 0usize; - let mut neighbor_mutual = 0usize; - let mut neighbor_non_mutual = 0usize; - - for &simplex_key in &new_simplices { - let Some(simplex) = self.tds.simplex(simplex_key) else { - continue; - }; - let Some(neighbors) = simplex.neighbor_keys() else { - continue; - }; - for neighbor_opt in neighbors { - total_slots = total_slots.saturating_add(1); - match neighbor_opt { - None => { - neighbor_none = neighbor_none.saturating_add(1); - } - Some(neighbor_key) => { - if !self.tds.contains_simplex(neighbor_key) { - neighbor_missing = neighbor_missing.saturating_add(1); - } else if self - .tds - .simplex(neighbor_key) - .and_then(Simplex::neighbors) - .is_some_and(|mut ns| { - ns.any(|neighbor| neighbor == Some(simplex_key)) - }) - { - neighbor_mutual = neighbor_mutual.saturating_add(1); - } else { - neighbor_non_mutual = neighbor_non_mutual.saturating_add(1); - } - } - } - } - } - - tracing::debug!( - new_simplices = new_simplices.len(), - total_slots, - neighbor_none, - neighbor_missing, - neighbor_mutual, - neighbor_non_mutual, - "Outside insertion: hull extension neighbor-pointer summary" - ); - } - - // Iteratively repair non-manifold topology until facet sharing is valid - let mut total_removed = 0; - let mut facet_sharing_known_valid = true; - let mut neighbor_repair_frontier = SimplexKeyBuffer::new(); - #[cfg_attr( - not(debug_assertions), - expect( - unused_variables, - reason = "`iteration` is only used for debug logging", - ) - )] - for iteration in 0..MAX_REPAIR_ITERATIONS { - // Check for non-manifold issues in newly created hull simplices (local scan) - // This keeps the repair O(k·D) where k is the number of new hull simplices, rather than O(N·D) - let simplices_to_check: SimplexKeyBuffer = new_simplices - .iter() - .copied() - .filter(|ck| self.tds.contains_simplex(*ck)) - .collect(); - - if let Some(issues) = self.detect_local_facet_issues(&simplices_to_check)? { - // Only mark this as "suspicious" if we *actually* detected local facet issues - // and entered the repair path. - suspicion.repair_loop_entered = true; - - #[cfg(debug_assertions)] - tracing::debug!( - "Hull extension repair iteration {}: {} over-shared facets detected, removing simplices...", - iteration + 1, - issues.len() - ); - - let repair = self.repair_local_facet_issues_with_frontier(&issues)?; - let removed = repair.removed_count; - - // Early exit if repair made no progress - if removed == 0 { - #[cfg(debug_assertions)] - tracing::warn!( - "No simplices removed in iteration {} - repair cannot make progress", - iteration + 1 - ); - return Err(InsertionError::TopologyValidation( - TdsError::InconsistentDataStructure { - message: format!( - "Hull extension repair stalled: {} over-shared facets remain but no simplices could be removed", - issues.len() - ), - }, - )); - } - - total_removed += removed; - neighbor_repair_frontier.extend(repair.frontier_simplices); - if removed > 0 { - suspicion.simplices_removed = true; - } - - #[cfg(debug_assertions)] - tracing::debug!( - removed_simplices = ?repair.removed_simplices, - "Removed {removed} simplices (total: {total_removed})" - ); - - // Early exit if repair succeeded - facet_sharing_known_valid = self.tds.validate_facet_sharing().is_ok(); - if facet_sharing_known_valid { - break; - } - } else { - // No more non-manifold issues - safe to rebuild neighbors - break; - } - } - - // Repair neighbor pointers now that topology is manifold. - if !facet_sharing_known_valid { - return Err(CavityFillingError::InvalidFacetSharingAfterRepair { - stage: CavityRepairStage::FanTriangulation, - } - .into()); - } - - if total_removed > 0 { - let repaired = self.repair_neighbors_after_local_simplex_removal( - &new_simplices, - &neighbor_repair_frontier, - )?; - suspicion.neighbor_pointers_rebuilt = repaired > 0; - } - - // New hull simplices were canonicalized on creation; validate the - // local orientation frontier without scanning the whole TDS. - let mut orientation_simplices = SimplexKeyBuffer::new(); - append_live_unique_simplex_seeds( - &self.tds, - &new_simplices, - &mut orientation_simplices, - ); - append_live_unique_simplex_seeds( - &self.tds, - &neighbor_repair_frontier, - &mut orientation_simplices, - ); - self.validate_local_orientation_for_simplices(&orientation_simplices)?; - - // Assign an incident simplex for the inserted vertex without a global rebuild. - let hint = new_simplices.iter().copied().find(|&ck| { - self.tds - .simplex(ck) - .is_some_and(|simplex| simplex.contains_vertex(v_key)) - }); - if let Some(incident_simplex) = hint - && let Some(vertex) = self.tds.vertex_mut(v_key) - { - vertex.set_incident_simplex(Some(incident_simplex)); - } - - #[cfg(debug_assertions)] - if std::env::var_os("DELAUNAY_DEBUG_RIDGE_LINK").is_some() { - match validate_ridge_links(&self.tds) { - Ok(()) => { - tracing::debug!( - "extend_hull: ridge-link validation passed after insertion" - ); - } - Err(err) => { - tracing::warn!( - error = ?err, - "extend_hull: ridge-link validation failed after insertion" - ); - } - } - } - - // Repair stale incident-simplex pointers (e.g. pointing to deleted - // conflict-region simplices) and error only for truly isolated vertices. - self.repair_stale_incident_simplices()?; - - // Connectedness guard (localized): ensure the newly created simplex set is internally - // connected and attached to the existing triangulation. - self.validate_connectedness(&new_simplices)?; - - // Return vertex key and hint for next insertion - let mut repair_seed_simplices = SimplexKeyBuffer::new(); - append_live_unique_simplex_seeds( - &self.tds, - &new_simplices, - &mut repair_seed_simplices, - ); - append_live_unique_simplex_seeds( - &self.tds, - &neighbor_repair_frontier, - &mut repair_seed_simplices, - ); - append_live_unique_simplex_seeds( - &self.tds, - &exterior_repair_seed_simplices, - &mut repair_seed_simplices, - ); - Ok(TryInsertImplOk { - inserted: (v_key, hint), - simplices_removed: total_removed, - suspicion, - repair_seed_simplices, - delaunay_repair_required: true, - }) - } - } - } - - /// Removes a vertex and retriangulates the resulting cavity using fan triangulation. - /// - /// This operation maintains topological consistency by: - /// 1. Finding all simplices containing the vertex - /// 2. Removing those simplices (creating a cavity) - /// 3. Extracting the cavity boundary facets - /// 4. Filling the cavity with a fan triangulation (pick apex, connect to all boundary facets) - /// 5. Wiring neighbors to maintain consistency - /// 6. Removing the vertex itself - /// - /// **Fan Triangulation**: The cavity is filled by picking one boundary vertex as an apex - /// and connecting it to all boundary facets. This is fast and maintains all topological - /// invariants, though it may create poorly-shaped simplices in some cases. - /// - /// # Arguments - /// - /// * `vertex_key` - Key of the vertex to remove - /// - /// # Returns - /// - /// The number of simplices that were removed along with the vertex. - /// - /// # Errors - /// - /// Returns [`InvariantError`] if the removal cannot be completed while maintaining - /// triangulation invariants. The error preserves structured information from whichever - /// layer (TDS or Topology) detected the failure. - pub(crate) fn remove_vertex(&mut self, vertex_key: VertexKey) -> Result { - // Verify the vertex exists - if self.tds.vertex(vertex_key).is_none() { - return Ok(0); // Vertex not found, nothing to remove - } - - // Collect all simplices containing this vertex by scanning all simplices - let simplices_to_remove: SimplexKeyBuffer = self - .tds - .simplices() - .filter_map(|(simplex_key, simplex)| { - if simplex.vertices().contains(&vertex_key) { - Some(simplex_key) - } else { - None - } - }) - .collect(); - - if simplices_to_remove.is_empty() { - // Vertex exists but has no incident simplices - use Tds removal - return self - .tds - .remove_vertex(vertex_key) - .map_err(|e| InvariantError::Tds(e.into_inner())); - } - - // Extract cavity boundary BEFORE removing simplices - let boundary_facets = - extract_cavity_boundary(&self.tds, &simplices_to_remove).map_err(|e| { - TdsError::InconsistentDataStructure { - message: format!("Failed to extract cavity boundary: {e}"), - } - })?; - - // If boundary is empty, we're removing the entire triangulation - if boundary_facets.is_empty() { - // Use Tds removal for empty boundary case - return self - .tds - .remove_vertex(vertex_key) - .map_err(|e| InvariantError::Tds(e.into_inner())); - } - - // Pick apex vertex for fan triangulation (first vertex of first boundary facet) - let apex_vertex_key = self.pick_fan_apex(&boundary_facets).ok_or_else(|| { - TdsError::InconsistentDataStructure { - message: "Failed to find apex vertex for fan triangulation".to_string(), - } - })?; - - // Snapshot before destructive retriangulation edits so we can roll back if any - // subsequent orientation/finalization step fails. - let tds_snapshot = self.tds.clone_for_rollback(); - let retriangulation_result = (|| -> Result { - // Fill cavity with fan triangulation BEFORE removing old simplices - // Use fan triangulation that skips boundary facets which already include the apex - let new_simplices = self - .fan_fill_cavity(apex_vertex_key, &boundary_facets) - .map_err(|e| insertion_error_to_invariant_error(e, "Fan triangulation failed"))?; - // Wire neighbors for the new simplices (while both old and new simplices exist) - let external_facets = - external_facets_for_boundary(&self.tds, &simplices_to_remove, &boundary_facets) - .map_err(|e| { - insertion_error_to_invariant_error(e, "External-facet collection failed") - })?; - wire_cavity_neighbors( - &mut self.tds, - &new_simplices, - external_facets.iter().copied(), - Some(&simplices_to_remove), - ) - .map_err(|e| insertion_error_to_invariant_error(e, "Neighbor wiring failed"))?; - - // Remove the simplices containing the vertex (now that new simplices are wired up) - // Note: remove_simplices_by_keys() automatically clears neighbor pointers in surviving - // simplices that reference removed simplices (sets them to None/boundary) - let mut simplices_removed = self.tds.remove_simplices_by_keys(&simplices_to_remove); - - // Validate facet topology for newly created simplices (O(k*D) localized check) - if let Some(issues) = self.detect_local_facet_issues(&new_simplices)? { - #[cfg(debug_assertions)] - tracing::warn!( - "Warning: {} over-shared facets detected after vertex removal, repairing...", - issues.len() - ); - let removed = self.repair_local_facet_issues(&issues)?; - simplices_removed += removed; - #[cfg(debug_assertions)] - tracing::debug!("Repaired by removing {removed} additional simplices"); - - // Repair neighbor pointers after removing additional simplices - // This ensures neighbor consistency after repair operations - if removed > 0 { - repair_neighbor_pointers(&mut self.tds).map_err(|e| { - insertion_error_to_invariant_error( - e, - "Neighbor repair after facet issue repair failed", - ) - })?; - } - } - // Normalize coherent orientation, canonicalize global sign, and promote - // simplices to positive orientation (#258). - self.normalize_and_promote_positive_orientation() - .map_err(|e| { - insertion_error_to_invariant_error( - e, - "Orientation canonicalization failed after fan retriangulation", - ) - })?; - - // Rebuild vertex-simplex incidence for all vertices - self.tds - .assign_incident_simplices() - .map_err(|e| InvariantError::Tds(e.into_inner()))?; - - // Remove the vertex using Tds method (handles internal bookkeeping) - self.tds - .remove_vertex(vertex_key) - .map_err(|e| InvariantError::Tds(e.into_inner()))?; - - Ok(simplices_removed) - })(); - - match retriangulation_result { - Ok(simplices_removed) => Ok(simplices_removed), - Err(error) => { - self.tds = tds_snapshot; - Err(error) - } - } - } - - /// Pick an apex vertex for fan triangulation. - /// - /// Selects the first vertex from the first boundary facet as the apex. - /// The fan will connect this apex to all boundary facets. - /// - /// # Arguments - /// - /// * `boundary_facets` - The cavity boundary facets - /// - /// # Returns - /// - /// The vertex key to use as apex, or None if no suitable vertex found. - fn pick_fan_apex(&self, boundary_facets: &[FacetHandle]) -> Option { - // Get first boundary facet - let first_facet = boundary_facets.first()?; - let simplex = self.tds.simplex(first_facet.simplex_key())?; - - // Get the first vertex from this facet (any vertex that's not the opposite one) - let facet_idx = >::from(first_facet.facet_index()); - simplex - .vertices() - .iter() - .enumerate() - .find(|(i, _)| *i != facet_idx) - .map(|(_, &vkey)| vkey) - } - - /// Fan-specific cavity fill: connect an existing apex vertex to boundary facets - /// that do not already include the apex. This avoids creating degenerate simplices - /// with duplicate vertices when the apex lies on a boundary facet. - fn fan_fill_cavity( - &mut self, - apex_vertex_key: VertexKey, - boundary_facets: &[FacetHandle], - ) -> Result { - let mut new_simplices = SimplexKeyBuffer::new(); - - for facet_handle in boundary_facets { - let boundary_simplex = - self.tds - .simplex(facet_handle.simplex_key()) - .ok_or_else(|| CavityFillingError::MissingBoundarySimplex { - simplex_key: facet_handle.simplex_key(), - })?; - - let facet_idx = >::from(facet_handle.facet_index()); - if facet_idx >= boundary_simplex.number_of_vertices() { - return Err(CavityFillingError::InvalidFacetIndex { - simplex_key: facet_handle.simplex_key(), - facet_index: facet_idx, - vertex_count: boundary_simplex.number_of_vertices(), - } - .into()); - } - - // Gather facet vertices (all except the opposite vertex) - let mut facet_vertices = SmallBuffer::::new(); - for (i, &vkey) in boundary_simplex.vertices().iter().enumerate() { - if i != facet_idx { - facet_vertices.push(vkey); - } - } - - // Skip facets that already contain the apex to avoid duplicate vertices - if facet_vertices.contains(&apex_vertex_key) { - continue; - } - - // Build new simplex vertices = facet_vertices + apex - let mut new_simplex_vertices = facet_vertices; - new_simplex_vertices.push(apex_vertex_key); - - // Create and insert the new simplex - let new_simplex = - Simplex::new(new_simplex_vertices, None).map_err(CavityFillingError::from)?; - let simplex_key = self - .tds - .insert_simplex_with_mapping_prechecked_topology(new_simplex) - .map_err(InsertionError::from)?; - - new_simplices.push(simplex_key); - } - - if new_simplices.is_empty() { - return Err(CavityFillingError::EmptyFanTriangulation.into()); - } - - Ok(new_simplices) - } - - /// Detects over-shared facets - /// - /// This is an **O(k * D)** operation where k = number of simplices to check, - /// unlike global validation which is O(N * D) for the entire triangulation. - /// - /// # Performance - /// - /// - **Complexity**: O(k * D) where k = `simplices.len()`, D = dimension - /// - **Use case**: Detect issues in newly created simplices after insertion/removal - /// - **Comparison**: Global detection is O(N * D) where N = total simplices - /// - /// # Arguments - /// - /// * `simplices` - Keys of simplices to check (typically newly created simplices) - /// - /// # Returns - /// - /// `Ok(None)` if all facets are valid (≤2 simplices per facet). - /// `Ok(Some(issues))` if over-shared facets are detected, where issues is a map - /// from facet hash to the simplices sharing that facet. - /// - /// # Errors - /// - /// Returns error if simplices cannot be accessed. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::*; - /// - /// // A single simplex has no over-shared facets. - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// let simplex_keys: Vec<_> = dt.simplices().map(|(ck, _)| ck).collect(); - /// let issues = dt - /// .as_triangulation() - /// .detect_local_facet_issues(&simplex_keys) - /// .unwrap(); - /// assert!(issues.is_none()); - /// - /// // Note: This method is most useful for checking newly created simplices - /// // after insertion/removal operations (see usage in insert_transactional). - /// ``` - pub fn detect_local_facet_issues( - &self, - simplices: &[SimplexKey], - ) -> Result, TdsError> { - // Build facet map for ONLY the specified simplices - // This is O(k * D) instead of O(N * D) - let mut facet_to_simplices = FacetIssuesMap::default(); - - // Index facets from the specified simplices - for &simplex_key in simplices { - let Some(simplex) = self.tds.simplex(simplex_key) else { - continue; // Simplex was removed, skip - }; - - // For each facet of this simplex - for facet_idx in 0..simplex.number_of_vertices() { - // Compute facet hash from sorted vertex keys - let mut facet_vkeys = SmallBuffer::::new(); - for (i, &vkey) in simplex.vertices().iter().enumerate() { - if i != facet_idx { - facet_vkeys.push(vkey); - } - } - facet_vkeys.sort_unstable(); - - // Hash the facet - let mut hasher = FastHasher::default(); - for &vkey in &facet_vkeys { - vkey.hash(&mut hasher); - } - let facet_hash = hasher.finish(); - - // Track this simplex/facet pair - let facet_idx_u8 = - u8::try_from(facet_idx).map_err(|_| TdsError::IndexOutOfBounds { - index: facet_idx, - bound: u8::MAX as usize + 1, - context: "facet index exceeds u8 range (dimension too high)".to_string(), - })?; - facet_to_simplices - .entry(facet_hash) - .or_insert_with(SmallBuffer::new) - .push((simplex_key, facet_idx_u8)); - } - } - - // Filter to only over-shared facets (> 2 simplices) in a single pass - facet_to_simplices.retain(|_, simplex_facet_pairs| simplex_facet_pairs.len() > 2); - - if facet_to_simplices.is_empty() { - Ok(None) - } else { - Ok(Some(facet_to_simplices)) - } - } - - /// Select simplices to remove for over-shared-facet repair without mutating the TDS. - fn simplices_for_local_facet_issue_repair( - &self, - issues: &FacetIssuesMap, - ) -> Result - where - K::Scalar: Div, - { - let mut simplices_to_remove = SimplexKeySet::default(); - - // For each over-shared facet, select simplices to remove - for simplex_facet_pairs in issues.values() { - // Compute quality for each simplex - propagate errors from quality evaluation - let mut simplex_qualities: Vec<(SimplexKey, f64, Uuid)> = Vec::new(); - for &(simplex_key, _) in simplex_facet_pairs { - let simplex = - self.tds - .simplex(simplex_key) - .ok_or_else(|| TdsError::SimplexNotFound { - simplex_key, - context: "facet repair quality evaluation".to_string(), - })?; - let uuid = simplex.uuid(); - - // Propagate quality evaluation errors - let ratio = radius_ratio(self, simplex_key).map_err(|e| { - TdsError::InconsistentDataStructure { - message: format!( - "Quality evaluation failed for simplex {simplex_key:?}: {e}" - ), - } - })?; - let ratio_f64 = - safe_scalar_to_f64(ratio).map_err(|_| TdsError::InconsistentDataStructure { - message: format!( - "Quality ratio conversion failed for simplex {simplex_key:?}" - ), - })?; - - if ratio_f64.is_finite() { - simplex_qualities.push((simplex_key, ratio_f64, uuid)); - } else { - return Err(TdsError::InconsistentDataStructure { - message: format!( - "Non-finite quality ratio {ratio_f64} for simplex {simplex_key:?}" - ), - }); - } - } - - // Quality-based selection: keep 2 best, remove rest - // Note: simplex_qualities always has all involved_simplices at this point since - // any quality computation failure results in an early error return above - simplex_qualities - .sort_unstable_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.2.cmp(&b.2))); - - // Mark simplices beyond the top 2 for removal - for (simplex_key, _, _) in simplex_qualities.iter().skip(2) { - if self.tds.contains_simplex(*simplex_key) { - simplices_to_remove.insert(*simplex_key); - } - } - } - - Ok(simplices_to_remove.into_iter().collect()) - } - - /// Collect surviving neighbor simplices that will have removed-simplex back-references cleared. - fn removal_frontier_for_simplices( - &self, - simplices_to_remove: &[SimplexKey], - ) -> SimplexKeyBuffer { - if simplices_to_remove.is_empty() { - return SimplexKeyBuffer::new(); - } - - let removal_set: SimplexKeySet = simplices_to_remove.iter().copied().collect(); - let mut frontier = SimplexKeyBuffer::new(); - let mut seen = FastHashSet::default(); - - for &simplex_key in simplices_to_remove { - let Some(simplex) = self.tds.simplex(simplex_key) else { - continue; - }; - let Some(neighbors) = simplex.neighbor_keys() else { - continue; - }; - for neighbor_key in neighbors.flatten() { - if removal_set.contains(&neighbor_key) || !self.tds.contains_simplex(neighbor_key) { - continue; - } - if seen.insert(neighbor_key) { - frontier.push(neighbor_key); - } - } - } - - frontier - } - - /// Add surviving simplices from the facet-issue incidence map to the local repair frontier. - fn add_issue_survivors_to_frontier( - &self, - issues: &FacetIssuesMap, - simplices_to_remove: &[SimplexKey], - frontier: &mut SimplexKeyBuffer, - ) { - let removal_set: SimplexKeySet = simplices_to_remove.iter().copied().collect(); - let mut seen: FastHashSet = frontier.iter().copied().collect(); - - for simplex_facet_pairs in issues.values() { - for &(simplex_key, _) in simplex_facet_pairs { - if removal_set.contains(&simplex_key) || !self.tds.contains_simplex(simplex_key) { - continue; - } - if seen.insert(simplex_key) { - frontier.push(simplex_key); - } - } - } - } - - /// Repair over-shared facets and return the local frontier for neighbor repair. - fn repair_local_facet_issues_with_frontier( - &mut self, - issues: &FacetIssuesMap, - ) -> Result - where - K::Scalar: Div, - { - let to_remove = self.simplices_for_local_facet_issue_repair(issues)?; - let mut frontier_simplices = self.removal_frontier_for_simplices(&to_remove); - self.add_issue_survivors_to_frontier(issues, &to_remove, &mut frontier_simplices); - let removed_count = self.tds.remove_simplices_by_keys(&to_remove); - - Ok(LocalFacetRepairOutcome { - removed_count, - removed_simplices: to_remove, - frontier_simplices, - }) - } - - /// Repairs over-shared facets by removing lower-quality simplices. - /// - /// Uses geometric quality metrics (`radius_ratio`) to select which simplices to keep - /// when a facet is shared by more than 2 simplices. UUID ordering is used as a tie-breaker - /// when simplices have equal quality. Errors if quality computation or conversion fails. - /// - /// # Performance - /// - /// - **Complexity**: O(m * q) where m = number of problematic facets, q = quality computation cost - /// - **Localized**: Only processes simplices involved in detected issues - /// - /// # Arguments - /// - /// * `issues` - Detected facet issues map from `detect_local_facet_issues()` - /// - /// # Returns - /// - /// Number of simplices removed during repair. - /// - /// # Errors - /// - /// Returns error if quality evaluation or facet bookkeeping fails while - /// selecting simplices to remove. This function itself does not rebuild neighbors; - /// callers are responsible for repairing or validating topology after removal - /// (e.g., via local or global neighbor-pointer repair, or a validation pass). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::collections::FacetIssuesMap; - /// use delaunay::prelude::tds::TdsError; - /// use delaunay::prelude::triangulation::*; - /// - /// # #[derive(Debug, thiserror::Error)] - /// # enum ExampleError { - /// # #[error(transparent)] - /// # Construction(#[from] DelaunayTriangulationConstructionError), - /// # #[error(transparent)] - /// # Tds(#[from] TdsError), - /// # } - /// # fn main() -> Result<(), ExampleError> { - /// // Start with a valid 2D simplex. - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices)?; - /// - /// // Empty issues map => nothing to remove. - /// let mut tri = dt.as_triangulation().clone(); - /// let removed = tri.repair_local_facet_issues(&FacetIssuesMap::default())?; - /// assert_eq!(removed, 0); - /// # Ok(()) - /// # } - /// ``` - /// - /// In practice, this method is typically called with issues detected by - /// [`detect_local_facet_issues`](Self::detect_local_facet_issues) after insertion/removal - /// operations. See `insert_transactional` for a typical usage pattern. - pub fn repair_local_facet_issues(&mut self, issues: &FacetIssuesMap) -> Result - where - K::Scalar: Div, - { - self.repair_local_facet_issues_with_frontier(issues) - .map(|outcome| outcome.removed_count) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::core::algorithms::locate::InternalInconsistencySite; - use crate::core::collections::NeighborBuffer; - use crate::core::collections::spatial_hash_grid::HashGridIndex; - use crate::core::simplex::NeighborSlot; - use crate::core::vertex::VertexBuilder; - use crate::geometry::kernel::{AdaptiveKernel, FastKernel}; - use crate::geometry::point::Point; - use crate::geometry::traits::coordinate::{ - Coordinate, CoordinateConversionError, CoordinateScalar, - }; - use crate::geometry::util::generate_random_points_seeded; - use crate::topology::characteristics::validation::validate_triangulation_euler; - use crate::topology::traits::topological_space::{GlobalTopology, ToroidalConstructionMode}; - use crate::triangulation::delaunay::{DelaunayRepairPolicy, DelaunayTriangulation}; - use crate::vertex; - - use slotmap::KeyData; - use std::collections::HashSet; - - /// Helper: build a minimal 3D triangulation with one tetrahedron and valid - /// incident-simplex pointers for all four vertices. - fn build_single_tet() -> ( - Triangulation, (), (), 3>, - [VertexKey; 4], - SimplexKey, - ) { - let mut tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - let v0 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) - .unwrap(); - let v1 = tri - .tds - .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) - .unwrap(); - let v2 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) - .unwrap(); - let v3 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) - .unwrap(); - - let ck = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap()) - .unwrap(); - - for vk in [v0, v1, v2, v3] { - tri.tds - .vertex_mut(vk) - .unwrap() - .set_incident_simplex(Some(ck)); - } - - (tri, [v0, v1, v2, v3], ck) - } - - struct ForceNextRetryableInsertionFailureGuard { - prior: bool, - } - - impl ForceNextRetryableInsertionFailureGuard { - fn enable() -> Self { - let prior = test_hooks::set_force_next_insertion_retryable_failure(true); - Self { prior } - } - } - - impl Drop for ForceNextRetryableInsertionFailureGuard { - fn drop(&mut self) { - test_hooks::restore_force_next_insertion_retryable_failure(self.prior); - } - } - - #[test] - fn test_triangulation_validation_error_try_from_manifold_error_preserves_detail() { - let tds_err = TdsError::InvalidNeighbors { - reason: NeighborValidationError::Other { - message: "unit test".to_string(), - }, - }; - - // ManifoldError::Tds belongs to the lower TDS layer, not TriangulationValidationError. - assert_eq!( - TriangulationValidationError::try_from(ManifoldError::Tds(tds_err.clone())), - Err(tds_err.clone()) - ); - assert_eq!( - InvariantError::from(ManifoldError::Tds(tds_err.clone())), - InvariantError::Tds(tds_err) - ); - - assert!(matches!( - TriangulationValidationError::try_from(ManifoldError::ManifoldFacetMultiplicity { - facet_key: 123, - simplex_count: 3 - }) - .unwrap(), - TriangulationValidationError::ManifoldFacetMultiplicity { - facet_key: 123, - simplex_count: 3 - } - )); - - assert!(matches!( - TriangulationValidationError::try_from(ManifoldError::BoundaryRidgeMultiplicity { - ridge_key: 0x00ab_cdef, - boundary_facet_count: 4 - }) - .unwrap(), - TriangulationValidationError::BoundaryRidgeMultiplicity { - ridge_key: 0x00ab_cdef, - boundary_facet_count: 4 - } - )); - - assert!(matches!( - TriangulationValidationError::try_from(ManifoldError::RidgeLinkNotManifold { - ridge_key: 0x00ab_cdef, - link_vertex_count: 7, - link_edge_count: 8, - max_degree: 3, - degree_one_vertices: 2, - connected: false - }) - .unwrap(), - TriangulationValidationError::RidgeLinkNotManifold { - ridge_key: 0x00ab_cdef, - link_vertex_count: 7, - link_edge_count: 8, - max_degree: 3, - degree_one_vertices: 2, - connected: false - } - )); - - assert!(matches!( - TriangulationValidationError::try_from(ManifoldError::VertexLinkNotManifold { - vertex_key: VertexKey::from(KeyData::from_ffi(1)), - link_vertex_count: 3, - link_simplex_count: 4, - boundary_facet_count: 1, - max_degree: 2, - connected: false, - interior_vertex: true, - }) - .unwrap(), - TriangulationValidationError::VertexLinkNotManifold { - link_vertex_count: 3, - link_simplex_count: 4, - boundary_facet_count: 1, - max_degree: 2, - connected: false, - interior_vertex: true, - .. - } - )); - } - - #[test] - fn test_internal_inconsistency_display() { - let err = TriangulationConstructionError::InternalInconsistency { - message: "missing vertex in lookup table".to_string(), - }; - - assert_eq!( - err.to_string(), - "Internal inconsistency during construction: missing vertex in lookup table" - ); - } - - #[test] - fn test_retryable_conflict_trace_detail_formats_retryable_variants() { - let extra_simplex = SimplexKey::from(KeyData::from_ffi(10)); - let disconnected_simplex = SimplexKey::from(KeyData::from_ffi(11)); - let open_simplex = SimplexKey::from(KeyData::from_ffi(12)); - - let non_manifold = InsertionError::ConflictRegion(ConflictError::NonManifoldFacet { - facet_hash: 0xABCD, - simplex_count: 3, - }); - assert_eq!( - retryable_conflict_trace_detail(&non_manifold).as_deref(), - Some("kind=non_manifold_facet facet_hash=0xabcd simplex_count=3") - ); - - let ridge_fan = InsertionError::ConflictRegion(ConflictError::RidgeFan { - facet_count: 4, - ridge_vertex_count: 2, - extra_simplices: vec![extra_simplex], - }); - assert_eq!( - retryable_conflict_trace_detail(&ridge_fan).as_deref(), - Some("kind=ridge_fan facet_count=4 ridge_vertex_count=2 extra_simplices=1") - ); - - let disconnected = InsertionError::ConflictRegion(ConflictError::DisconnectedBoundary { - visited: 2, - total: 5, - disconnected_simplices: vec![disconnected_simplex], - }); - assert_eq!( - retryable_conflict_trace_detail(&disconnected).as_deref(), - Some("kind=disconnected_boundary visited=2 total=5 disconnected_simplices=1") - ); - - let open = InsertionError::ConflictRegion(ConflictError::OpenBoundary { - facet_count: 1, - ridge_vertex_count: 2, - open_simplex, - }); - assert_eq!( - retryable_conflict_trace_detail(&open).as_deref(), - Some("kind=open_boundary facet_count=1 ridge_vertex_count=2") - ); - - let not_retryable = InsertionError::CavityFilling { - reason: CavityFillingError::EmptyFanTriangulation, - }; - assert!(retryable_conflict_trace_detail(¬_retryable).is_none()); - } - - #[test] - fn test_cavity_conflict_error_summary_formats_all_variants() { - let simplex_key = SimplexKey::from(KeyData::from_ffi(21)); - - let cases = vec![ - ( - ConflictError::NonManifoldFacet { - facet_hash: 0xCAFE, - simplex_count: 4, - }, - "non_manifold_facet facet_hash=0xcafe simplex_count=4".to_string(), - ), - ( - ConflictError::RidgeFan { - facet_count: 5, - ridge_vertex_count: 3, - extra_simplices: vec![simplex_key], - }, - "ridge_fan facet_count=5 ridge_vertex_count=3 extra_simplices=1".to_string(), - ), - ( - ConflictError::DisconnectedBoundary { - visited: 1, - total: 3, - disconnected_simplices: vec![simplex_key], - }, - "disconnected_boundary visited=1 total=3 disconnected_simplices=1".to_string(), - ), - ( - ConflictError::OpenBoundary { - facet_count: 1, - ridge_vertex_count: 2, - open_simplex: simplex_key, - }, - format!( - "open_boundary facet_count=1 ridge_vertex_count=2 open_simplex={simplex_key:?}" - ), - ), - ( - ConflictError::InvalidStartSimplex { simplex_key }, - format!("invalid_start_simplex simplex_key={simplex_key:?}"), - ), - ( - ConflictError::SimplexDataAccessFailed { - simplex_key, - message: "missing vertices".to_string(), - }, - format!( - "simplex_data_access_failed simplex_key={simplex_key:?} message=missing vertices" - ), - ), - ]; - - for (error, expected) in cases { - assert_eq!(cavity_conflict_error_summary(&error), expected); - } - - let predicate = ConflictError::PredicateError { - source: CoordinateConversionError::ConversionFailed { - coordinate_index: 2, - coordinate_value: "NaN".to_string(), - from_type: "f64", - to_type: "f32", - }, - }; - assert!( - cavity_conflict_error_summary(&predicate) - .starts_with("predicate_error source=Failed to convert coordinate") - ); - - let internal = ConflictError::InternalInconsistency { - site: InternalInconsistencySite::RidgeInfoMissingSecondFacet { - first_facet: 4, - boundary_facets_len: 6, - ridge_vertex_count: 2, - }, - }; - assert!(cavity_conflict_error_summary(&internal).contains("internal_inconsistency site=")); - } - - #[test] - fn test_log_cavity_reduction_event_only_evaluates_when_enabled() { - let mut conflict_simplices = SimplexKeyBuffer::new(); - conflict_simplices.push(SimplexKey::from(KeyData::from_ffi(41))); - - let mut called = false; - log_cavity_reduction_event(false, 0, &conflict_simplices, || { - called = true; - "should not run".to_string() - }); - assert!(!called); - - log_cavity_reduction_event(true, 1, &conflict_simplices, || { - called = true; - "ran".to_string() - }); - assert!(called); - } - - #[test] - fn test_triangulation_new_empty_and_new_with_tds_default_to_pl_manifold() { - let tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - assert_eq!(tri.topology_guarantee(), TopologyGuarantee::PLManifold); - - let tri_with_tds: Triangulation, (), (), 2> = - Triangulation::new_with_tds(FastKernel::new(), Tds::::empty()); - assert_eq!( - tri_with_tds.topology_guarantee(), - TopologyGuarantee::PLManifold - ); - } - - #[test] - fn test_triangulation_set_topology_guarantee_round_trips() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - assert_eq!(tri.topology_guarantee(), TopologyGuarantee::PLManifold); - - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - assert_eq!(tri.topology_guarantee(), TopologyGuarantee::Pseudomanifold); - - tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); - assert_eq!( - tri.topology_guarantee(), - TopologyGuarantee::PLManifoldStrict - ); - - tri.set_topology_guarantee(TopologyGuarantee::PLManifold); - assert_eq!(tri.topology_guarantee(), TopologyGuarantee::PLManifold); - } - #[test] - fn test_validation_policy_should_validate_matrix() { - let clean = SuspicionFlags::default(); - let suspicious = SuspicionFlags { - perturbation_used: true, - ..SuspicionFlags::default() - }; - - assert!(!ValidationPolicy::Never.should_validate(clean)); - assert!(!ValidationPolicy::Never.should_validate(suspicious)); - - assert!(ValidationPolicy::Always.should_validate(clean)); - assert!(ValidationPolicy::Always.should_validate(suspicious)); - - assert!(!ValidationPolicy::OnSuspicion.should_validate(clean)); - assert!(ValidationPolicy::OnSuspicion.should_validate(suspicious)); - - assert!(ValidationPolicy::DebugOnly.should_validate(suspicious)); - assert_eq!( - ValidationPolicy::DebugOnly.should_validate(clean), - cfg!(debug_assertions) - ); - } - - #[test] - fn test_topology_guarantee_helper_matrix_and_policy_compatibility() { - assert_eq!(TopologyGuarantee::default(), TopologyGuarantee::DEFAULT); - assert_eq!(TopologyGuarantee::DEFAULT, TopologyGuarantee::PLManifold); - - assert!(!TopologyGuarantee::Pseudomanifold.requires_vertex_links_during_insertion()); - assert!(TopologyGuarantee::PLManifoldStrict.requires_vertex_links_during_insertion()); - - assert!(!TopologyGuarantee::Pseudomanifold.requires_vertex_links_at_completion()); - assert!(TopologyGuarantee::PLManifold.requires_vertex_links_at_completion()); - assert!(TopologyGuarantee::PLManifoldStrict.requires_vertex_links_at_completion()); - - assert!( - TopologyGuarantee::Pseudomanifold.requires_pseudomanifold_checks_during_insertion() - ); - assert!(TopologyGuarantee::PLManifold.requires_pseudomanifold_checks_during_insertion()); - assert!( - TopologyGuarantee::PLManifoldStrict.requires_pseudomanifold_checks_during_insertion() - ); - - assert!(!TopologyGuarantee::Pseudomanifold.requires_ridge_links()); - assert!(TopologyGuarantee::PLManifold.requires_ridge_links()); - assert!(TopologyGuarantee::PLManifoldStrict.requires_ridge_links()); - - // default_validation_policy - assert_eq!( - TopologyGuarantee::PLManifoldStrict.default_validation_policy(), - ValidationPolicy::Always - ); - assert_eq!( - TopologyGuarantee::PLManifold.default_validation_policy(), - ValidationPolicy::OnSuspicion - ); - assert_eq!( - TopologyGuarantee::Pseudomanifold.default_validation_policy(), - ValidationPolicy::OnSuspicion - ); - - // Verify constructors use the centralized mapping. - let tri = Triangulation::, (), (), 2>::new_empty(FastKernel::new()); - assert_eq!( - tri.validation_policy(), - TopologyGuarantee::DEFAULT.default_validation_policy() - ); - - for policy in [ - ValidationPolicy::Never, - ValidationPolicy::OnSuspicion, - ValidationPolicy::Always, - ValidationPolicy::DebugOnly, - ] { - assert!(TopologyGuarantee::Pseudomanifold.is_compatible_with_policy(policy)); - } - - assert!(!TopologyGuarantee::PLManifold.is_compatible_with_policy(ValidationPolicy::Never)); - assert!( - !TopologyGuarantee::PLManifoldStrict.is_compatible_with_policy(ValidationPolicy::Never) - ); - assert!( - TopologyGuarantee::PLManifold.is_compatible_with_policy(ValidationPolicy::OnSuspicion) - ); - assert!(TopologyGuarantee::PLManifold.is_compatible_with_policy(ValidationPolicy::Always)); - assert!( - TopologyGuarantee::PLManifoldStrict.is_compatible_with_policy(ValidationPolicy::Always) - ); - } - - #[test] - fn test_set_validation_policy_incompatible_updates_when_completion_validation_succeeds() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - assert_eq!(tri.topology_guarantee(), TopologyGuarantee::PLManifold); - assert_eq!(tri.validation_policy(), ValidationPolicy::OnSuspicion); - - tri.set_validation_policy(ValidationPolicy::Never); - assert_eq!(tri.validation_policy(), ValidationPolicy::Never); - } - - #[test] - fn test_set_topology_guarantee_incompatible_updates_when_completion_validation_succeeds() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - tri.set_validation_policy(ValidationPolicy::Never); - assert_eq!(tri.validation_policy(), ValidationPolicy::Never); - assert_eq!(tri.topology_guarantee(), TopologyGuarantee::PLManifold); - - tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); - assert_eq!( - tri.topology_guarantee(), - TopologyGuarantee::PLManifoldStrict - ); - } - - #[test] - fn test_duplicate_detection_metrics_force_enable() { - struct DuplicateDetectionGuard; - - impl Drop for DuplicateDetectionGuard { - fn drop(&mut self) { - DUPLICATE_DETECTION_FORCE_ENABLED.store(false, Ordering::Relaxed); - } - } - - let _guard = DuplicateDetectionGuard; - DUPLICATE_DETECTION_FORCE_ENABLED.store(true, Ordering::Relaxed); - - let before = Triangulation::, (), (), 2>::duplicate_detection_metrics() - .expect("duplicate detection metrics should be enabled"); - - record_duplicate_detection_metrics(true, 3, false); - record_duplicate_detection_metrics(false, 0, true); - - let after = Triangulation::, (), (), 2>::duplicate_detection_metrics() - .expect("duplicate detection metrics should be enabled"); - - assert!(after.total_checks > before.total_checks); - assert!(after.grid_used > before.grid_used); - assert!(after.grid_fallbacks > before.grid_fallbacks); - assert!(after.grid_candidates >= before.grid_candidates + 3); - } - - #[test] - fn test_validate_at_completion_skips_for_pseudomanifold() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - assert!(tri.validate_at_completion().is_ok()); - } - /// Regression test: a negatively oriented but topologically valid simplex - /// passes `is_valid_topology_only()` while failing `is_valid()` (which - /// includes the geometric orientation check). - #[test] - fn test_negative_oriented_simplex_topology_only() { - // Build a single positively oriented triangle, then swap vertices 0↔1 - // to make it negatively oriented. - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - // Confirm it starts valid. - assert!(tri.is_valid().is_ok()); - assert!(tri.is_valid_topology_only().is_ok()); - - // Flip the single simplex's vertex order to make it negatively oriented. - let simplex_key = tri - .tds - .simplex_keys() - .next() - .expect("single simplex exists"); - tri.tds - .simplex_mut(simplex_key) - .expect("simplex exists") - .swap_vertex_slots(0, 1); - - // Topology-only validation should still pass (orientation sign is irrelevant). - assert!( - tri.is_valid_topology_only().is_ok(), - "Negatively oriented simplex should pass topology-only validation" - ); - - // Full validation (which includes geometric orientation) should fail. - assert!( - tri.is_valid().is_err(), - "Negatively oriented simplex should fail full is_valid()" - ); - } - - fn insert_test_vertex_with_coords( - tds: &mut Tds, - entries: &[(usize, f64)], - ) -> VertexKey { - let mut coords = [0.0_f64; D]; - for &(axis, value) in entries { - coords[axis] = value; - } - tds.insert_vertex_with_mapping(vertex!(coords)).unwrap() - } - - fn build_invalid_vertex_link_tds() -> (Tds, VertexKey) { - // Two disjoint stars sharing only one apex produce a disconnected vertex link. - let mut tds: Tds = Tds::empty(); - let shared = insert_test_vertex_with_coords(&mut tds, &[]); - - if D == 2 { - // Two cone cycles keep the 1D boundary closed, so strict validation reaches - // the disconnected vertex-link diagnostic instead of stopping at ridge degree. - let first_a = insert_test_vertex_with_coords(&mut tds, &[(0, 1.0)]); - let first_b = insert_test_vertex_with_coords(&mut tds, &[(1, 1.0)]); - let first_c = insert_test_vertex_with_coords(&mut tds, &[(0, -1.0)]); - let second_a = insert_test_vertex_with_coords(&mut tds, &[(0, 10.0)]); - let second_b = insert_test_vertex_with_coords(&mut tds, &[(0, 11.0), (1, 1.0)]); - let second_c = insert_test_vertex_with_coords(&mut tds, &[(0, 9.0), (1, 1.0)]); - - for simplex_vertices in [ - vec![shared, first_a, first_b], - vec![shared, first_b, first_c], - vec![shared, first_c, first_a], - vec![shared, second_a, second_b], - vec![shared, second_b, second_c], - vec![shared, second_c, second_a], - ] { - let _ = tds - .insert_simplex_with_mapping(Simplex::new(simplex_vertices, None).unwrap()) - .unwrap(); - } - - tds.assign_incident_simplices().unwrap(); - return (tds, shared); - } - - let mut first_simplex_vertices = vec![shared]; - for axis in 0..D { - let mut coords = [0.0_f64; D]; - coords[axis] = 1.0; - first_simplex_vertices.push(tds.insert_vertex_with_mapping(vertex!(coords)).unwrap()); - } - - let mut second_simplex_vertices = vec![shared]; - for axis in 0..D { - let mut coords = [0.0_f64; D]; - coords[0] = 10.0; - coords[axis] += 1.0; - second_simplex_vertices.push(tds.insert_vertex_with_mapping(vertex!(coords)).unwrap()); - } - - let _ = tds - .insert_simplex_with_mapping(Simplex::new(first_simplex_vertices, None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(second_simplex_vertices, None).unwrap()) - .unwrap(); - - tds.assign_incident_simplices().unwrap(); - - (tds, shared) - } - - fn build_invalid_vertex_link_tds_2d() -> (Tds, VertexKey) { - build_invalid_vertex_link_tds::<2>() - } - - #[test] - fn test_validate_at_completion_reports_invalid_vertex_link() { - let (tds, v0) = build_invalid_vertex_link_tds_2d(); - - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - tri.set_topology_guarantee(TopologyGuarantee::PLManifold); - - match tri.validate_at_completion() { - Err(InvariantError::Triangulation( - TriangulationValidationError::VertexLinkNotManifold { vertex_key, .. }, - )) => { - assert_eq!(vertex_key, v0); - } - other => panic!("Expected VertexLinkNotManifold, got {other:?}"), - } - } - #[test] - fn test_set_validation_policy_rejects_incompatible_policy_when_completion_validation_fails() { - let (tds, _) = build_invalid_vertex_link_tds_2d(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - assert!(matches!( - tri.validate_at_completion(), - Err(InvariantError::Triangulation( - TriangulationValidationError::VertexLinkNotManifold { .. } - )) - )); - assert_eq!(tri.validation_policy(), ValidationPolicy::OnSuspicion); - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - tri.set_validation_policy(ValidationPolicy::Never); - })); - if cfg!(debug_assertions) { - assert!(result.is_err()); - } else { - assert!(result.is_ok()); - } - assert_eq!(tri.validation_policy(), ValidationPolicy::OnSuspicion); - } - - #[test] - fn test_set_topology_guarantee_rejects_incompatible_guarantee_when_completion_validation_fails() - { - let (tds, _) = build_invalid_vertex_link_tds_2d(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - tri.set_validation_policy(ValidationPolicy::Never); - assert_eq!(tri.topology_guarantee(), TopologyGuarantee::Pseudomanifold); - assert_eq!(tri.validation_policy(), ValidationPolicy::Never); - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); - })); - if cfg!(debug_assertions) { - assert!(result.is_err()); - } else { - assert!(result.is_ok()); - } - assert_eq!(tri.topology_guarantee(), TopologyGuarantee::Pseudomanifold); - } - - fn build_disconnected_two_triangles_tds_2d() -> Tds { - let mut tds: Tds = Tds::empty(); - - let a0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); - let a1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); - let a2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); - - let b0 = tds - .insert_vertex_with_mapping(vertex!([10.0, 0.0])) - .unwrap(); - let b1 = tds - .insert_vertex_with_mapping(vertex!([11.0, 0.0])) - .unwrap(); - let b2 = tds - .insert_vertex_with_mapping(vertex!([10.0, 1.0])) - .unwrap(); - - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![a0, a1, a2], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![b0, b1, b2], None).unwrap()) - .unwrap(); - - tds - } - - fn build_three_triangles_sharing_edge_tds_2d() -> Tds { - let mut tds: Tds = Tds::empty(); - - let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); - let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); - let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); - let v3 = tds - .insert_vertex_with_mapping(vertex!([0.0, -1.0])) - .unwrap(); - let v4 = tds.insert_vertex_with_mapping(vertex!([2.0, 0.0])).unwrap(); - - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v3], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_bypassing_topology_checks_for_test( - Simplex::new(vec![v0, v1, v4], None).unwrap(), - ) - .unwrap(); - - tds - } - - #[test] - fn test_validate_after_insertion_skips_when_no_simplices() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - // Force validation to be enabled if there were any simplices. - tri.set_validation_policy(ValidationPolicy::Always); - - // Insert a vertex without creating any simplices (bootstrap phase). - let _ = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0])) - .unwrap(); - assert_eq!(tri.number_of_simplices(), 0); - - tri.validate_after_insertion_with_scope(SuspicionFlags::default(), None) - .unwrap(); - } - - #[test] - fn test_validate_after_insertion_calls_is_valid_when_policy_triggers() { - let tds = build_disconnected_two_triangles_tds_2d(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - tri.set_validation_policy(ValidationPolicy::Always); - - match tri.validate_after_insertion_with_scope(SuspicionFlags::default(), None) { - Err(InvariantError::Triangulation(TriangulationValidationError::Disconnected { - .. - })) => {} - other => panic!("Expected Disconnected error, got {other:?}"), - } - } - - #[test] - fn test_validate_after_insertion_required_checks_do_not_run_global_connectedness() { - let tds = build_disconnected_two_triangles_tds_2d(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - tri.set_validation_policy(ValidationPolicy::OnSuspicion); - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - - // The triangulation is globally invalid (disconnected), but the required - // pseudomanifold checks are local and still satisfied. - assert!(tri.is_valid().is_err()); - tri.validate_after_insertion_with_scope(SuspicionFlags::default(), None) - .unwrap(); - } - - #[test] - fn test_validate_after_insertion_does_not_skip_pseudomanifold_checks() { - let tds = build_three_triangles_sharing_edge_tds_2d(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - tri.set_validation_policy(ValidationPolicy::OnSuspicion); - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - - match tri.validate_after_insertion_with_scope(SuspicionFlags::default(), None) { - Err(InvariantError::Triangulation( - TriangulationValidationError::ManifoldFacetMultiplicity { simplex_count, .. }, - )) => { - assert_eq!(simplex_count, 3); - } - other => panic!("Expected ManifoldFacetMultiplicity, got {other:?}"), - } - } - - #[test] - fn test_scoped_validation_catches_touched_over_shared_facet() { - let tds = build_three_triangles_sharing_edge_tds_2d(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let scope: SimplexKeyBuffer = tri.tds.simplex_keys().take(1).collect(); - - tri.set_validation_policy(ValidationPolicy::OnSuspicion); - tri.set_topology_guarantee(TopologyGuarantee::PLManifold); - - match tri.validate_after_insertion_with_scope(SuspicionFlags::default(), Some(&scope)) { - Err(InvariantError::Triangulation( - TriangulationValidationError::ManifoldFacetMultiplicity { simplex_count, .. }, - )) => { - assert_eq!(simplex_count, 3); - } - other => panic!("Expected ManifoldFacetMultiplicity, got {other:?}"), - } - } - - fn unit_simplex_vertices() -> Vec> { - let mut vertices = Vec::with_capacity(D + 1); - vertices.push(vertex!([0.0_f64; D])); - for axis in 0..D { - let mut coords = [0.0_f64; D]; - coords[axis] = 1.0; - vertices.push(vertex!(coords)); - } - vertices - } - - fn unit_simplex_interior_vertex() -> Vertex { - vertex!([0.125_f64; D]) - } - - /// Build a simplex whose feature length is controlled by one shared axis scale. - fn axis_scaled_simplex_vertices(scale: f64) -> Vec> { - let mut vertices = Vec::with_capacity(D + 1); - vertices.push(vertex!([0.0_f64; D])); - for axis in 0..D { - let mut coords = [0.0_f64; D]; - coords[axis] = scale; - vertices.push(vertex!(coords)); - } - vertices - } - - /// Build coordinates with only the first component set for tolerance-scale tests. - fn coords_with_first(first: f64) -> [f64; D] { - let mut coords = [0.0_f64; D]; - coords[0] = first; - coords - } - - macro_rules! test_scoped_strict_validation_falls_back_to_global_vertex_links { - ($($dim:expr),+ $(,)?) => { - pastey::paste! { - $( - #[test] - fn []() { - let (tds, expected_vertex_key) = build_invalid_vertex_link_tds::<$dim>(); - let mut tri = - Triangulation::, (), (), $dim>::new_with_tds(FastKernel::new(), tds); - let scope: SimplexKeyBuffer = tri.tds.simplex_keys().take(1).collect(); - assert!(!scope.is_empty()); - - // Direct field assignment keeps this internal test focused on insertion-time - // strict fallback behavior even though the fixture is intentionally invalid. - tri.validation_policy = ValidationPolicy::OnSuspicion; - tri.topology_guarantee = TopologyGuarantee::PLManifoldStrict; - - match tri.validate_after_insertion_with_scope(SuspicionFlags::default(), Some(&scope)) { - Err(InvariantError::Triangulation( - TriangulationValidationError::RidgeLinkNotManifold { - connected: false, - .. - }, - )) if $dim == 2 => { - // In 2D, ridges are vertices, so the global strict path - // reports the disconnected apex link at the ridge layer first. - } - Err(InvariantError::Triangulation( - TriangulationValidationError::VertexLinkNotManifold { vertex_key, .. }, - )) => { - assert_eq!(vertex_key, expected_vertex_key); - } - other => panic!("Expected VertexLinkNotManifold, got {other:?}"), - } - } - )+ - } - }; - } - - test_scoped_strict_validation_falls_back_to_global_vertex_links!(2, 3, 4, 5); - - #[test] - fn test_local_geometric_orientation_validation_errors_on_missing_scope_simplex() { - 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 tds = - Triangulation::, (), (), 3>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); - let simplex_key = tri.tds.simplex_keys().next().unwrap(); - assert_eq!(tri.tds.remove_simplices_by_keys(&[simplex_key]), 1); - - match tri.validate_geometric_simplex_orientation_for_simplices(&[simplex_key]) { - Err(TdsError::SimplexNotFound { - simplex_key: missing_key, - .. - }) => assert_eq!(missing_key, simplex_key), - other => panic!("Expected SimplexNotFound, got {other:?}"), - } - } - - macro_rules! test_insertion_scoped_validation_preserves_full_validity { - ($($dim:expr),+ $(,)?) => { - pastey::paste! { - $( - #[test] - fn []() { - let vertices = unit_simplex_vertices::<$dim>(); - let tds = - Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) - .unwrap(); - let mut tri = - Triangulation::, (), (), $dim>::new_with_tds(FastKernel::new(), tds); - - tri.set_validation_policy(ValidationPolicy::OnSuspicion); - tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); - - let detail = tri - .insert_with_statistics_seeded_indexed_detailed( - unit_simplex_interior_vertex::<$dim>(), - None, - None, - 0, - None, - None, - ) - .unwrap(); - - assert!(matches!( - detail.outcome, - InsertionOutcome::Inserted { - vertex_key: _, - hint: _ - } - )); - assert!(!detail.repair_seed_simplices.is_empty()); - tri.validate_after_insertion_with_scope( - SuspicionFlags::default(), - Some(&detail.repair_seed_simplices), - ) - .unwrap(); - tri.is_valid().unwrap(); - } - )+ - } - }; - } - - test_insertion_scoped_validation_preserves_full_validity!(2, 3, 4, 5); - - #[test] - fn test_validate_after_insertion_skips_global_validation_but_runs_required_checks() { - let tds = build_disconnected_two_triangles_tds_2d(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - tri.set_validation_policy(ValidationPolicy::OnSuspicion); - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - - assert!(tri.is_valid().is_err()); - tri.validate_after_insertion_with_scope(SuspicionFlags::default(), None) - .unwrap(); - } - - #[test] - fn test_validation_after_insertion_will_run_matches_policy_and_link_requirements() { - let tds = build_disconnected_two_triangles_tds_2d(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - tri.set_validation_policy(ValidationPolicy::OnSuspicion); - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - assert_eq!( - tri.validation_after_insertion_work(SuspicionFlags::default()), - Some(InsertionValidationWork::RequiredTopologyLinks) - ); - - tri.set_topology_guarantee(TopologyGuarantee::PLManifold); - assert_eq!( - tri.validation_after_insertion_work(SuspicionFlags::default()), - Some(InsertionValidationWork::RequiredTopologyLinks) - ); - } - - #[test] - fn test_select_locate_hint_from_hash_grid_returns_incident_simplex() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let simplex_key = tri.tds.simplex_keys().next().unwrap(); - - let mut index: HashGridIndex = HashGridIndex::new(1.0); - for (vkey, vertex) in tri.tds.vertices() { - index.insert_vertex(vkey, vertex.point().coords()); - } - - let hint = tri.select_locate_hint_from_hash_grid(&[0.05, 0.05], &index); - assert_eq!(hint, Some(simplex_key)); - } - - #[test] - fn test_select_locate_hint_from_hash_grid_skips_missing_simplex() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - let vkey = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0])) - .unwrap(); - { - let vertex = tri.tds.vertex_mut(vkey).unwrap(); - vertex.set_incident_simplex(Some(SimplexKey::default())); - } - - let mut index: HashGridIndex = HashGridIndex::new(1.0); - index.insert_vertex(vkey, &[0.0, 0.0]); - - let hint = tri.select_locate_hint_from_hash_grid(&[0.0, 0.0], &index); - assert!(hint.is_none()); - } - - #[test] - fn test_duplicate_coordinates_error_uses_hash_grid_index() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - let vkey = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0])) - .unwrap(); - - let mut index: HashGridIndex = HashGridIndex::new(1.0); - index.insert_vertex(vkey, &[0.0, 0.0]); - - let tol = 1e-10_f64; - let err = tri.duplicate_coordinates_error(&[0.0, 0.0], tol, Some(&index)); - assert!(matches!( - err, - Some(InsertionError::DuplicateCoordinates { .. }) - )); - } - - #[test] - fn test_duplicate_coordinates_error_falls_back_when_index_unusable() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - let _ = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0])) - .unwrap(); - - let index: HashGridIndex = HashGridIndex::new(0.0); // unusable - let tol = 1e-10_f64; - let err = tri.duplicate_coordinates_error(&[0.0, 0.0], tol, Some(&index)); - assert!(matches!( - err, - Some(InsertionError::DuplicateCoordinates { .. }) - )); - } - - fn duplicate_coordinate_tolerance_scales_down_for_small_features() { - let mut tri: Triangulation, (), (), D> = - Triangulation::new_empty(FastKernel::new()); - let _ = tri - .tds - .insert_vertex_with_mapping(vertex!(coords_with_first::(1.0e-6))) - .unwrap(); - - let candidate = coords_with_first::(1.0e-6 + 1.0e-11); - let tolerance = tri.estimate_duplicate_coordinate_tolerance(&candidate, None); - - assert!( - tolerance < 1.0e-10, - "small-scale inputs should not inherit a fixed scalar-unit tolerance" - ); - assert!( - tri.duplicate_coordinates_error(&candidate, tolerance, None) - .is_none(), - "distinct small-scale vertices should not be skipped as duplicates" - ); - } - - fn duplicate_coordinate_tolerance_uses_hint_simplex_span() { - let vertices = unit_simplex_vertices::(); - let tds = - Triangulation::, (), (), D>::build_initial_simplex(&vertices).unwrap(); - let tri = Triangulation::, (), (), D>::new_with_tds(FastKernel::new(), tds); - let hint = tri.tds.simplex_keys().next(); - let candidate = coords_with_first::(5.0e-11); - let tolerance = tri.estimate_duplicate_coordinate_tolerance(&candidate, hint); - - assert!( - tolerance > 1.0e-10, - "unit-scale hint simplices should preserve near-duplicate filtering" - ); - assert!(matches!( - tri.duplicate_coordinates_error(&candidate, tolerance, None), - Some(InsertionError::DuplicateCoordinates { .. }) - )); - } - - fn duplicate_index_rebuilds_when_tolerance_exceeds_cell_size() { - let vertices = axis_scaled_simplex_vertices::(1.0e6); - let tds = - Triangulation::, (), (), D>::build_initial_simplex(&vertices).unwrap(); - let tri = Triangulation::, (), (), D>::new_with_tds(FastKernel::new(), tds); - let hint = tri.tds.simplex_keys().next(); - let candidate = [1.0_f64; D]; - let tolerance = tri.estimate_duplicate_coordinate_tolerance(&candidate, hint); - let mut index: HashGridIndex = HashGridIndex::new(1.0e-10); - for (vkey, vertex) in tri.tds.vertices() { - index.insert_vertex(vkey, vertex.point().coords()); - } - - tri.ensure_duplicate_index_cell_size(Some(&mut index), tolerance); - - approx::assert_abs_diff_eq!(index.cell_size(), tolerance, epsilon = f64::EPSILON); - assert!( - index.for_each_candidate_vertex_key(&candidate, |_| false), - "rebuilt duplicate index should remain queryable" - ); - } - - #[test] - fn test_duplicate_distance_within_tolerance_handles_overflowed_tolerance_square() { - assert!( - Triangulation::, (), (), 2>::duplicate_distance_within_tolerance( - f64::MAX, - f64::MAX - ) - ); - assert!( - !Triangulation::, (), (), 2>::duplicate_distance_within_tolerance( - f64::MAX, - 1.0 - ) - ); - } - - macro_rules! test_duplicate_tolerance_dimensions { - ($($dim:expr),+ $(,)?) => { - pastey::paste! { - $( - #[test] - fn []() { - duplicate_coordinate_tolerance_scales_down_for_small_features::<$dim>(); - } - - #[test] - fn []() { - duplicate_coordinate_tolerance_uses_hint_simplex_span::<$dim>(); - } - - #[test] - fn []() { - duplicate_index_rebuilds_when_tolerance_exceeds_cell_size::<$dim>(); - } - )+ - } - }; - } - - test_duplicate_tolerance_dimensions!(2, 3, 4, 5); - - #[test] - fn test_estimate_local_perturbation_scale_uses_hint_simplex_vertices() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let simplex_key = tri.tds.simplex_keys().next().unwrap(); - - let scale = tri.estimate_local_perturbation_scale(&[0.1, 0.0], Some(simplex_key)); - assert!((scale - 0.1).abs() < 1e-12); - } - - #[test] - fn test_estimate_local_perturbation_scale_clamps_to_min_scale() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - let _ = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0])) - .unwrap(); - - let scale = tri.estimate_local_perturbation_scale(&[0.0, 0.0], None); - let min_scale = ::default_tolerance(); - approx::assert_abs_diff_eq!(scale, min_scale, epsilon = f64::EPSILON); - } - - #[test] - fn test_pl_manifold_insertion_never_commits_invalid_topology_under_validation_policy_never() { - // A small deterministic point set (seeded RNG) that exercises degeneracy handling. - // - // This test is intentionally small and validates after *each* insertion to ensure - // we never commit an invalid PL-manifold state, even when the user disables - // automatic validation via `ValidationPolicy::Never`. - let points = generate_random_points_seeded::(25, (-100.0, 100.0), 123).unwrap(); - - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::empty_with_topology_guarantee(TopologyGuarantee::PLManifold); - - dt.set_validation_policy(ValidationPolicy::Never); - dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); - - for (i, point) in points.into_iter().enumerate() { - let vertex = VertexBuilder::default().point(point).build().unwrap(); - - let result = dt - .insert_with_statistics(vertex) - .unwrap_or_else(|e| panic!("Non-retryable insertion error at i={i}: {e:?}")); - - let (outcome, stats) = result; - - // Skip Level 3 validation during bootstrap (vertices but no simplices yet). - if dt.number_of_simplices() > 0 - && let Err(err) = dt.as_triangulation().validate() - { - panic!( - "Topology invalid after insertion i={i} (outcome={outcome:?}, attempts={}, used_perturbation={}): {err}", - stats.attempts, - stats.used_perturbation() - ); - } - } - } - - /// Macro to generate `build_initial_simplex` tests across dimensions. - /// - /// This macro generates tests that verify `build_initial_simplex` by: - /// 1. Creating D+1 affinely independent vertices - /// 2. Calling `build_initial_simplex` directly - /// 3. Verifying the Tds has correct structure (vertices, simplices, dimension) - /// - /// # Usage - /// ```ignore - /// test_build_initial_simplex!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); - /// ``` - macro_rules! test_build_initial_simplex { - ($dim:expr, [$($simplex_coords:expr),+ $(,)?]) => { - pastey::paste! { - #[test] - fn []() { - // Build initial simplex (D+1 vertices) - let vertices: Vec> = vec![ - $(vertex!($simplex_coords)),+ - ]; - - let expected_vertices = vertices.len(); - assert_eq!(expected_vertices, $dim + 1, - "Test must provide exactly D+1 vertices for {}D simplex", $dim); - - let tds = Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) - .unwrap(); - - // Verify structure - assert_eq!(tds.number_of_vertices(), expected_vertices, - "{}D: Expected {} vertices", $dim, expected_vertices); - assert_eq!(tds.number_of_simplices(), 1, - "{}D: Expected 1 simplex", $dim); - assert_eq!(tds.dim(), $dim as i32, - "{}D: Expected dimension {}", $dim, $dim); - - // Verify all vertices are present - assert_eq!(tds.vertices().count(), expected_vertices, - "{}D: All vertices should be in Tds", $dim); - - // Verify the single simplex has correct number of vertices - let (_, simplex) = tds.simplices().next() - .expect(&format!("{}D: Should have exactly one simplex", $dim)); - assert_eq!(simplex.number_of_vertices(), expected_vertices, - "{}D: Simplex should have {} vertices", $dim, expected_vertices); - - // Verify incident simplices are assigned - for (_, vertex) in tds.vertices() { - assert!(vertex.incident_simplex().is_some(), - "{}D: All vertices should have incident simplex assigned", $dim); - } - - // Verify initial simplex has explicit boundary neighbor slots. - let neighbors = simplex - .neighbor_slots() - .expect("initial simplex should assign boundary neighbor slots"); - assert!( - neighbors.iter().all(|slot| *slot == NeighborSlot::Boundary), - "{}D: Initial simplex should have boundary slots", - $dim - ); - } - } - }; - } - - // 2D: Triangle - test_build_initial_simplex!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); - - // 3D: Tetrahedron - test_build_initial_simplex!( - 3, - [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0] - ] - ); - - // 4D: 4-simplex - test_build_initial_simplex!( - 4, - [ - [0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0] - ] - ); - - // 5D: 5-simplex - test_build_initial_simplex!( - 5, - [ - [0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0] - ] - ); - - /// Macro to generate Level 3 (topology) validation tests across dimensions. - /// - /// This macro generates tests that verify manifold-with-boundary validation by: - /// 1. Creating a Delaunay triangulation from D+1 affinely independent vertices - /// 2. Calling `Triangulation::is_valid()` (Level 3) - /// 3. Verifying that the validation passes - /// - /// # Usage - /// ```ignore - /// test_is_valid_topology!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); - /// ``` - macro_rules! test_is_valid_topology { - ($dim:expr, [$($simplex_coords:expr),+ $(,)?]) => { - pastey::paste! { - #[test] - fn []() { - // Build triangulation from D+1 vertices (initial simplex) - let vertices: Vec> = vec![ - $(vertex!($simplex_coords)),+ - ]; - - let expected_vertices = vertices.len(); - assert_eq!(expected_vertices, $dim + 1, - "Test must provide exactly D+1 vertices for {}D simplex", $dim); - - let dt = DelaunayTriangulation::new(&vertices) - .expect(&format!("Failed to create {}D triangulation", $dim)); - let tri = dt.as_triangulation(); - - // Level 3: topology validation - let result = tri.is_valid(); - assert!( - result.is_ok(), - "{}D: Simple simplex should be a valid manifold-with-boundary. Error: {:?}", - $dim, - result.err() - ); - - // Also verify basic properties - assert_eq!(tri.number_of_vertices(), expected_vertices, - "{}D: Should have {} vertices", $dim, expected_vertices); - assert_eq!(tri.number_of_simplices(), 1, - "{}D: Should have exactly 1 simplex", $dim); - } - } - }; - } - - // 2D: Triangle manifold - test_is_valid_topology!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); - - // 3D: Tetrahedron manifold - test_is_valid_topology!( - 3, - [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0] - ] - ); - - // 4D: 4-simplex manifold - test_is_valid_topology!( - 4, - [ - [0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0] - ] - ); - - // 5D: 5-simplex manifold - test_is_valid_topology!( - 5, - [ - [0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0] - ] - ); - - #[test] - fn test_is_valid_topology_empty() { - // Empty triangulation should pass topology validation - let tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - assert!( - tri.is_valid().is_ok(), - "Empty triangulation should be a valid (empty) manifold" - ); - } - - #[test] - fn test_is_valid_pl_manifold_mode_rejects_wedge_at_vertex_in_2d() { - // This builds the same 2D "wedge at a vertex" configuration as the topology-module - // unit test, but exercises the Level 3 validation pipeline and TopologyGuarantee gating. - // - // The complex is a pseudomanifold (every edge has degree 2), but not a PL 2-manifold: - // the shared vertex has a disconnected link (two disjoint cycles). - let mut tds: Tds = Tds::empty(); - - // Shared vertex. - let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); - - // First tetrahedron boundary (4 triangles on 4 vertices). - let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); - let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); - let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap(); - - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v3], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v2, v3], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v1, v2, v3], None).unwrap()) - .unwrap(); - - // Second tetrahedron boundary (shares only v0). - let v4 = tds - .insert_vertex_with_mapping(vertex!([10.0, 10.0])) - .unwrap(); - let v5 = tds - .insert_vertex_with_mapping(vertex!([11.0, 10.0])) - .unwrap(); - let v6 = tds - .insert_vertex_with_mapping(vertex!([10.0, 11.0])) - .unwrap(); - - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v4, v5], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v4, v6], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v5, v6], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v4, v5, v6], None).unwrap()) - .unwrap(); - - // Ensure neighbor pointers exist so connectedness validation is meaningful. - repair_neighbor_pointers(&mut tds).unwrap(); - - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - // Default is PL-manifold mode; relax to pseudomanifold for this part of the test. - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - - // In pseudomanifold mode, Level 3 validation proceeds past manifold checks and fails at - // connectedness (two components that share only a vertex). - assert!(matches!( - tri.is_valid(), - Err(InvariantError::Triangulation( - TriangulationValidationError::Disconnected { .. } - )) - )); - - tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); - - // In strict PL-manifold mode, Level 3 validation should fail. With connectivity now - // checked first, a disconnected triangulation surfaces as Disconnected before vertex-link - // or ridge-link validation. The two components share only v0 (a vertex, not a facet) - // so the neighbor graph is disconnected. - match tri.is_valid() { - Err(InvariantError::Triangulation( - TriangulationValidationError::VertexLinkNotManifold { - vertex_key, - link_vertex_count, - link_simplex_count, - boundary_facet_count, - max_degree, - connected, - interior_vertex, - }, - )) => { - assert_eq!(vertex_key, v0); - assert!(interior_vertex); - assert!(link_vertex_count > 0); - assert!(link_simplex_count > 0); - assert_eq!(boundary_facet_count, 0); - assert_eq!(max_degree, 2); - assert!(!connected); - } - // Connectivity is checked before link validation; a two-component wedge is - // also disconnected in the neighbor graph, so either error is acceptable. - Err(InvariantError::Triangulation( - TriangulationValidationError::RidgeLinkNotManifold { .. } - | TriangulationValidationError::Disconnected { .. }, - )) => {} - other => panic!( - "Expected RidgeLinkNotManifold, VertexLinkNotManifold, or Disconnected in strict PL-manifold mode, got {other:?}" - ), - } - } - - #[test] - fn test_is_valid_pl_manifold_mode_rejects_cone_on_torus_in_3d_even_when_simplex_graph_connected() - { - // Cone over a triangulated torus: - // - The 3D simplex neighbor graph is connected. - // - Facet-degree and closed-boundary checks pass. - // - But the apex has link T^2 (χ=0), so PL-manifold vertex-link validation must fail. - const N: usize = 3; - const M: usize = 3; - - let mut tds: Tds = Tds::empty(); - - let mut v: [[VertexKey; M]; N] = [[VertexKey::from(KeyData::from_ffi(0)); M]; N]; - for (i, row) in v.iter_mut().enumerate() { - for (j, slot) in row.iter_mut().enumerate() { - let i_f = >::from(u32::try_from(i).unwrap()); - let j_f = >::from(u32::try_from(j).unwrap()); - *slot = tds - .insert_vertex_with_mapping(vertex!([i_f, j_f, 0.0])) - .unwrap(); - } - } - - let apex = tds - .insert_vertex_with_mapping(vertex!([0.5, 0.5, 1.0])) - .unwrap(); - - for i in 0..N { - for j in 0..M { - let i1 = (i + 1) % N; - let j1 = (j + 1) % M; - - let v00 = v[i][j]; - let v10 = v[i1][j]; - let v01 = v[i][j1]; - let v11 = v[i1][j1]; - - for tri in [[v00, v10, v01], [v10, v11, v01]] { - let _ = tds - .insert_simplex_with_mapping( - Simplex::new(vec![tri[0], tri[1], tri[2], apex], None).unwrap(), - ) - .unwrap(); - } - } - } - - repair_neighbor_pointers(&mut tds).unwrap(); - - let mut tri = - Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); - - // Sanity: the simplex neighbor graph is connected. - tri.validate_global_connectedness().unwrap(); - - // Sanity: pseudomanifold-with-boundary checks pass. - let facet_to_simplices = tri.tds.build_facet_to_simplices_map().unwrap(); - validate_facet_degree(&facet_to_simplices).unwrap(); - validate_closed_boundary(&tri.tds, &facet_to_simplices).unwrap(); - - tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); - - match tri.is_valid() { - Err(InvariantError::Triangulation( - TriangulationValidationError::VertexLinkNotManifold { - vertex_key, - link_vertex_count, - link_simplex_count, - boundary_facet_count, - connected, - interior_vertex, - .. - }, - )) => { - assert_eq!(vertex_key, apex); - assert!(interior_vertex); - assert!(connected); - assert_eq!(boundary_facet_count, 0); - assert!(link_vertex_count > 0); - assert!(link_simplex_count > 0); - } - other => panic!("Expected VertexLinkNotManifold for cone apex, got {other:?}"), - } - } - - #[test] - fn test_is_valid_disconnected_detected_before_non_manifold_boundary_ridge() { - // Two tetrahedra that share only an edge (not a facet) are disconnected in the neighbor - // graph (no shared facet ⇒ no neighbor pointers). Connectivity is now checked FIRST in - // `is_valid()`, so the disconnection error is returned before the non-manifold boundary - // ridge error (4 boundary triangles on the shared edge). - let mut tds: Tds = Tds::empty(); - - // Shared edge - let shared_edge_v0 = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) - .unwrap(); - let shared_edge_v1 = tds - .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) - .unwrap(); - - // First tetrahedron - let tet1_v2 = tds - .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) - .unwrap(); - let tet1_v3 = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) - .unwrap(); - - // Second tetrahedron - let tet2_v2 = tds - .insert_vertex_with_mapping(vertex!([0.0, -1.0, 0.0])) - .unwrap(); - let tet2_v3 = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, -1.0])) - .unwrap(); - - let _ = tds - .insert_simplex_with_mapping( - Simplex::new(vec![shared_edge_v0, shared_edge_v1, tet1_v2, tet1_v3], None).unwrap(), - ) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping( - Simplex::new(vec![shared_edge_v0, shared_edge_v1, tet2_v2, tet2_v3], None).unwrap(), - ) - .unwrap(); - - let tri = Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); - - // The two simplices share only an edge, so they have no mutual neighbor pointers and the - // neighbor-graph BFS visits only one of them. Connectivity is now the first check in - // `is_valid()`, so the disconnection error is returned first. - match tri.is_valid() { - Err(InvariantError::Triangulation(TriangulationValidationError::Disconnected { - simplex_count, - })) => { - assert_eq!( - simplex_count, 2, - "Expected 2 simplices in disconnected triangulation" - ); - } - other => panic!("Expected Disconnected, got {other:?}"), - } - } - #[test] - fn test_validate_includes_tds_validation() { - 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::new(&vertices).unwrap(); - let tri = dt.as_triangulation(); - - // Triangulation::validate should pass if the underlying TDS validates. - assert!(tri.tds.validate().is_ok(), "TDS should validate"); - assert!( - tri.validate().is_ok(), - "Triangulation::validate should pass" - ); - } - - #[test] - fn test_is_valid_rejects_bootstrap_phase_with_isolated_vertex() { - // A triangulation with vertices but no simplices is not a valid manifold (Level 3). - // Level 3 requires every vertex to be incident to at least one simplex. - let mut tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - // Bootstrap insertion (no simplices yet) - let vertex = vertex!([0.0, 0.0, 0.0]); - let expected_uuid = vertex.uuid(); - let (expected_vk, _) = tri - .insert(vertex, None, None) - .expect("bootstrap insertion should succeed"); - - match tri.is_valid() { - Err(InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { - vertex_key, - vertex_uuid, - })) => { - assert_eq!(vertex_key, expected_vk); - assert_eq!(vertex_uuid, expected_uuid); - } - other => panic!("Expected IsolatedVertex error, got {other:?}"), - } - } - - #[test] - fn test_is_valid_rejects_isolated_vertex_even_when_simplices_exist() { - 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 tds = - Triangulation::, (), (), 3>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); - - // Default is PL-manifold mode; use pseudomanifold mode here so the isolated-vertex check - // triggers before vertex-link validation. - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - - // Insert a vertex into the TDS without adding any simplices that reference it. - // This creates an isolated vertex, which violates the Level 3 manifold invariant. - let _isolated_vk = tri - .tds - .insert_vertex_with_mapping(vertex!([10.0, 10.0, 10.0])) - .unwrap(); - - match tri.is_valid() { - Err(InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { - .. - })) => { - // Expected: isolated vertex produces a structured IsolatedVertex error. - } - other => panic!("Expected IsolatedVertex error, got {other:?}"), - } - } - - #[test] - fn test_is_valid_rejects_disconnected_even_when_euler_matches() { - // Construct a disconnected 1D triangulation made of: - // - A path (Ball(1)) with χ = 1 - // - A cycle (ClosedSphere(1)) with χ = 0 - // - // The overall complex has boundary, so it is classified as Ball(1) with expected χ = 1. - // Euler characteristic alone therefore cannot detect disconnectedness here. - let mut tds: Tds = Tds::empty(); - - // Path component: v0 - v1 - v2 (2 edges) - let v0 = tds.insert_vertex_with_mapping(vertex!([0.0])).unwrap(); - let v1 = tds.insert_vertex_with_mapping(vertex!([1.0])).unwrap(); - let v2 = tds.insert_vertex_with_mapping(vertex!([2.0])).unwrap(); - - let e0 = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1], None).unwrap()) - .unwrap(); - let e1 = tds - .insert_simplex_with_mapping(Simplex::new(vec![v1, v2], None).unwrap()) - .unwrap(); - - // Cycle component: v3 - v4 - v5 - v3 (3 edges) - let v3 = tds.insert_vertex_with_mapping(vertex!([10.0])).unwrap(); - let v4 = tds.insert_vertex_with_mapping(vertex!([11.0])).unwrap(); - let v5 = tds.insert_vertex_with_mapping(vertex!([12.0])).unwrap(); - - let c0 = tds - .insert_simplex_with_mapping(Simplex::new(vec![v3, v4], None).unwrap()) - .unwrap(); - let c1 = tds - .insert_simplex_with_mapping(Simplex::new(vec![v4, v5], None).unwrap()) - .unwrap(); - let c2 = tds - .insert_simplex_with_mapping(Simplex::new(vec![v5, v3], None).unwrap()) - .unwrap(); - - // Set neighbor pointers (1D: each simplex has 2 "facets" => 2 neighbor slots). - - // Path neighbors: - { - let simplex = tds.simplex_mut(e0).unwrap(); - let mut neighbors = NeighborBuffer::>::new(); - neighbors.resize(2, None); - // e0 = [v0, v1]; across v1 is facet_index=0 - neighbors[0] = Some(e1); - simplex.set_neighbors_from_keys(neighbors).unwrap(); - } - { - let simplex = tds.simplex_mut(e1).unwrap(); - let mut neighbors = NeighborBuffer::>::new(); - neighbors.resize(2, None); - // e1 = [v1, v2]; across v1 is facet_index=1 - neighbors[1] = Some(e0); - simplex.set_neighbors_from_keys(neighbors).unwrap(); - } - - // Cycle neighbors: - { - let simplex = tds.simplex_mut(c0).unwrap(); - let mut neighbors = NeighborBuffer::>::new(); - neighbors.resize(2, None); - // c0 = [v3, v4]; across v4 is facet_index=0, across v3 is facet_index=1 - neighbors[0] = Some(c1); // at v4 - neighbors[1] = Some(c2); // at v3 - simplex.set_neighbors_from_keys(neighbors).unwrap(); - } - { - let simplex = tds.simplex_mut(c1).unwrap(); - let mut neighbors = NeighborBuffer::>::new(); - neighbors.resize(2, None); - // c1 = [v4, v5]; across v5 is facet_index=0, across v4 is facet_index=1 - neighbors[0] = Some(c2); // at v5 - neighbors[1] = Some(c0); // at v4 - simplex.set_neighbors_from_keys(neighbors).unwrap(); - } - { - let simplex = tds.simplex_mut(c2).unwrap(); - let mut neighbors = NeighborBuffer::>::new(); - neighbors.resize(2, None); - // c2 = [v5, v3]; across v3 is facet_index=0, across v5 is facet_index=1 - neighbors[0] = Some(c0); // at v3 - neighbors[1] = Some(c1); // at v5 - simplex.set_neighbors_from_keys(neighbors).unwrap(); - } - - tds.assign_incident_simplices().unwrap(); - - let tri = Triangulation::, (), (), 1>::new_with_tds(FastKernel::new(), tds); - - // Sanity: codimension-1 pseudomanifold facet multiplicity passes. - let facet_to_simplices = tri.tds.build_facet_to_simplices_map().unwrap(); - validate_facet_degree(&facet_to_simplices).unwrap(); - - // Sanity: Euler characteristic check would pass for this disconnected complex. - let topology = validate_triangulation_euler(&tri.tds).unwrap(); - assert_eq!( - topology.classification, - TopologyClassification::Ball(1), - "Classification should be Ball(1) because the complex has boundary" - ); - assert_eq!(topology.expected, Some(1)); - assert_eq!(topology.chi, 1); - - // Level 3 should still fail due to disconnectedness. - match tri.is_valid() { - Err(InvariantError::Triangulation(TriangulationValidationError::Disconnected { - simplex_count, - })) => { - assert_eq!( - simplex_count, 5, - "Expected 5 simplices (2 path + 3 cycle) in disconnected triangulation" - ); - } - other => panic!("Expected Disconnected, got {other:?}"), - } - } - - #[test] - fn test_tds_is_valid_rejects_boundary_facet_has_neighbor() { - // Create two disjoint tetrahedra and manually introduce an invalid neighbor pointer - // across a boundary facet. - let vertices_simplex_1 = 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 mut tds = - Triangulation::, (), (), 3>::build_initial_simplex(&vertices_simplex_1) - .unwrap(); - let first_simplex_key = tds.simplex_keys().next().unwrap(); - - // Add a disjoint second tetrahedron. - let v4 = tds - .insert_vertex_with_mapping(vertex!([10.0, 0.0, 0.0])) - .unwrap(); - let v5 = tds - .insert_vertex_with_mapping(vertex!([11.0, 0.0, 0.0])) - .unwrap(); - let v6 = tds - .insert_vertex_with_mapping(vertex!([10.0, 1.0, 0.0])) - .unwrap(); - let v7 = tds - .insert_vertex_with_mapping(vertex!([10.0, 0.0, 1.0])) - .unwrap(); - - let simplex_2 = Simplex::new(vec![v4, v5, v6, v7], None).unwrap(); - let second_simplex_key = tds.insert_simplex_with_mapping(simplex_2).unwrap(); - - // Invalidate: boundary facet has a neighbor pointer. - let first_simplex = tds.simplex_mut(first_simplex_key).unwrap(); - let mut neighbors = NeighborBuffer::>::new(); - neighbors.resize(4, None); - neighbors[0] = Some(second_simplex_key); - first_simplex.set_neighbors_from_keys(neighbors).unwrap(); - - match tds.is_valid() { - Err(TdsError::InvalidNeighbors { - reason: NeighborValidationError::BoundaryFacetHasNeighbor { neighbor_key, .. }, - }) => { - assert_eq!(neighbor_key, second_simplex_key); - } - other => panic!("Expected InvalidNeighbors, got {other:?}"), - } - } - - #[test] - fn test_tds_is_valid_rejects_interior_facet_neighbor_mismatch() { - // Two tetrahedra share a facet, but we leave neighbor pointers unset. - let mut tds: Tds = Tds::empty(); - - let v0 = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) - .unwrap(); - let v1 = tds - .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) - .unwrap(); - let v2 = tds - .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) - .unwrap(); - let v3 = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) - .unwrap(); - let v4 = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 2.0])) - .unwrap(); - - let simplex_1 = Simplex::new(vec![v0, v1, v2, v3], None).unwrap(); - let simplex_2 = Simplex::new(vec![v0, v1, v2, v4], None).unwrap(); - let _ = tds.insert_simplex_with_mapping(simplex_1).unwrap(); - let _ = tds.insert_simplex_with_mapping(simplex_2).unwrap(); - - match tds.is_valid() { - Err(TdsError::InvalidNeighbors { - reason: NeighborValidationError::InteriorFacetNeighborMismatch { .. }, - }) => {} - other => panic!("Expected InvalidNeighbors, got {other:?}"), - } - } - - #[test] - fn test_is_valid_non_manifold_facet_multiplicity() { - // Three tetrahedra share a single facet -> not a manifold-with-boundary. - let mut tds: Tds = Tds::empty(); - - let v0 = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) - .unwrap(); - let v1 = tds - .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) - .unwrap(); - let v2 = tds - .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) - .unwrap(); - let v3 = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) - .unwrap(); - let v4 = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 2.0])) - .unwrap(); - let v5 = tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 3.0])) - .unwrap(); - - let simplex_1 = Simplex::new(vec![v0, v1, v2, v3], None).unwrap(); - let simplex_2 = Simplex::new(vec![v0, v1, v2, v4], None).unwrap(); - let simplex_3 = Simplex::new(vec![v0, v1, v2, v5], None).unwrap(); - - let _ = tds.insert_simplex_with_mapping(simplex_1).unwrap(); - let _ = tds.insert_simplex_with_mapping(simplex_2).unwrap(); - let _ = tds - .insert_simplex_bypassing_topology_checks_for_test(simplex_3) - .unwrap(); - - let tri = Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); - - // The three simplices share facet {v0,v1,v2} three-ways. `repair_neighbor_pointers` would - // fail on this configuration (a facet shared by >2 simplices violates the early-exit guard in - // `assign_neighbors`), so the simplices have no neighbor pointers and the neighbor-graph BFS - // visits only one of them. Connectivity is now the first check in `is_valid()`, so the - // disconnection error is returned before the non-manifold facet error. - match tri.is_valid() { - Err(InvariantError::Triangulation(TriangulationValidationError::Disconnected { - .. - })) => {} - // Non-manifold facet detection is still valid if the simplices happen to be connected. - Err(InvariantError::Triangulation( - TriangulationValidationError::ManifoldFacetMultiplicity { simplex_count, .. }, - )) => { - assert_eq!(simplex_count, 3); - } - other => panic!("Expected Disconnected or ManifoldFacetMultiplicity, got {other:?}"), - } - } - - #[test] - fn test_triangulation_validation_report_ok_for_valid_simplex() { - 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 tds = - Triangulation::, (), (), 3>::build_initial_simplex(&vertices).unwrap(); - let tri = Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); - - assert!(tri.validation_report().is_ok()); - } - - #[test] - fn test_triangulation_validation_report_returns_mapping_failures_only() { - 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 tds = - Triangulation::, (), (), 3>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); - - // Break UUID↔key mappings: remove one vertex UUID entry. - let uuid = tri.tds.vertices().next().unwrap().1.uuid(); - tri.tds.uuid_to_vertex_key.remove(&uuid); - - let report = tri.validation_report().unwrap_err(); - assert!(!report.violations.is_empty()); - assert!(report.violations.iter().all(|v| { - matches!( - v.kind, - InvariantKind::VertexMappings | InvariantKind::SimplexMappings - ) - })); - } - - #[test] - fn test_triangulation_validation_report_includes_vertex_and_simplex_validity() { - 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 tds = - Triangulation::, (), (), 3>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); - - // Insert an invalid vertex (nil UUID) to exercise VertexValidity reporting. - let invalid_vertex: Vertex = Vertex::empty(); - let _ = tri.tds.insert_vertex_with_mapping(invalid_vertex).unwrap(); - - // Corrupt one simplex locally: neighbors buffer with the wrong length. - let simplex_key = tri.tds.simplex_keys().next().unwrap(); - let simplex = tri.tds.simplex_mut(simplex_key).unwrap(); - simplex.ensure_neighbors_buffer_mut().truncate(3); // expected D+1 = 4 - - let report = tri.validation_report().unwrap_err(); - - assert!( - report - .violations - .iter() - .any(|v| v.kind == InvariantKind::VertexValidity), - "Report should include a VertexValidity violation" - ); - assert!( - report - .violations - .iter() - .any(|v| v.kind == InvariantKind::SimplexValidity), - "Report should include a SimplexValidity violation" - ); - } - - #[test] - fn test_insert_duplicate_coordinates_skips_with_statistics_and_errors_without() { - let mut tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - // First insertion succeeds. - tri.insert(vertex!([0.0, 0.0, 0.0]), None, None) - .expect("first insertion should succeed"); - assert_eq!(tri.number_of_vertices(), 1); - - // Second insertion at same coordinates: insert() returns Err, insert_with_statistics() reports Skipped. - let err = tri - .insert(vertex!([0.0, 0.0, 0.0]), None, None) - .unwrap_err(); - assert!(matches!(err, InsertionError::DuplicateCoordinates { .. })); - - let (outcome, stats) = tri - .insert_with_statistics(vertex!([0.0, 0.0, 0.0]), None, None) - .unwrap(); - assert!(stats.skipped()); - assert!(matches!(outcome, InsertionOutcome::Skipped { .. })); - - // No new vertex should have been inserted. - assert_eq!(tri.number_of_vertices(), 1); - } - - #[test] - fn test_insert_duplicate_uuid_is_non_retryable_and_rolls_back() { - // Insert a vertex, then attempt to insert another vertex with the same UUID. - let mut tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - tri.insert(vertex!([0.0, 0.0, 0.0]), None, None) - .expect("first insertion should succeed"); - assert_eq!(tri.number_of_vertices(), 1); - - let existing_uuid = tri.tds.vertices().next().unwrap().1.uuid(); - let mut dup = vertex!([1.0, 0.0, 0.0]); - dup.set_uuid(existing_uuid).unwrap(); - - let err = tri.insert(dup, None, None).unwrap_err(); - assert!( - !err.is_retryable(), - "Duplicate UUID should be non-retryable" - ); - - // Ensure rollback: vertex count unchanged. - assert_eq!(tri.number_of_vertices(), 1); - } - - #[test] - fn test_build_initial_simplex_insufficient_vertices() { - // Try to build 3D simplex with only 2 vertices (need 4) - let vertices = vec![vertex!([0.0, 0.0, 0.0]), vertex!([1.0, 0.0, 0.0])]; - - let result = Triangulation::, (), (), 3>::build_initial_simplex(&vertices); - - assert!(result.is_err()); - match result { - Err(TriangulationConstructionError::InsufficientVertices { dimension, .. }) => { - assert_eq!(dimension, 3); - } - _ => panic!("Expected InsufficientVertices error"), - } - } - - #[test] - fn test_build_initial_simplex_too_many_vertices() { - // Try to build 2D simplex with 4 vertices (need exactly 3) - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([0.5, 0.5]), - ]; - - let result = Triangulation::, (), (), 2>::build_initial_simplex(&vertices); - - assert!(result.is_err()); - match result { - Err(TriangulationConstructionError::InsufficientVertices { .. }) => {} - _ => panic!("Expected InsufficientVertices error for wrong count"), - } - } - - fn invalid_initial_simplex_vertices() -> Vec> { - let mut vertices = Vec::with_capacity(D + 1); - vertices.push(vertex!([0.0_f64; D])); - - let mut invalid_coords = [0.0_f64; D]; - invalid_coords[0] = 1.0; - invalid_coords[1] = f64::NAN; - vertices.push(Vertex::new_with_uuid( - Point::new(invalid_coords), - Uuid::new_v4(), - None, - )); - - for axis in 1..D { - let mut coords = [0.0_f64; D]; - coords[axis] = 1.0; - vertices.push(vertex!(coords)); - } - - vertices - } - - macro_rules! test_build_initial_simplex_rejects_invalid_vertex_dimensions { - ($($dim:expr),+ $(,)?) => { - pastey::paste! { - $( - #[test] - fn []() { - let vertices = invalid_initial_simplex_vertices::<$dim>(); - - let result = Triangulation::, (), (), $dim>::build_initial_simplex(&vertices); - - assert!(matches!( - result, - Err(TriangulationConstructionError::Tds( - TdsConstructionError::ValidationError(TdsError::InvalidVertex { .. }) - )) - )); - } - )+ - } - }; - } - - test_build_initial_simplex_rejects_invalid_vertex_dimensions!(2, 3, 4, 5); - - #[test] - fn test_build_initial_simplex_with_user_data() { - // Build vertices with user data - let v1 = VertexBuilder::default() - .point(Point::new([0.0, 0.0])) - .data(42_usize) - .build() - .unwrap(); - let v2 = VertexBuilder::default() - .point(Point::new([1.0, 0.0])) - .data(43_usize) - .build() - .unwrap(); - let v3 = VertexBuilder::default() - .point(Point::new([0.0, 1.0])) - .data(44_usize) - .build() - .unwrap(); - - let vertices = vec![v1, v2, v3]; - let tds = Triangulation::, usize, (), 2>::build_initial_simplex(&vertices) - .unwrap(); - - assert_eq!(tds.number_of_vertices(), 3); - assert_eq!(tds.number_of_simplices(), 1); - - // Verify user data is preserved - let data_values: Vec<_> = tds - .vertices() - .filter_map(|(_, v)| v.data.as_ref()) - .copied() - .collect(); - assert_eq!(data_values.len(), 3); - assert!(data_values.contains(&42)); - assert!(data_values.contains(&43)); - assert!(data_values.contains(&44)); - } - - // ============================================================================= - // Tests for build_initial_simplex degeneracy validation - // ============================================================================= - - #[test] - fn test_build_initial_simplex_rejects_collinear_2d() { - // Collinear points should be rejected by build_initial_simplex - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([2.0, 0.0]), - ]; - - let result = Triangulation::, (), (), 2>::build_initial_simplex(&vertices); - - assert!(result.is_err(), "Collinear points should be rejected"); - match result { - Err(TriangulationConstructionError::GeometricDegeneracy { message }) => { - assert!( - message.contains("Degenerate"), - "Error message should mention degeneracy" - ); - } - _ => panic!("Expected GeometricDegeneracy error for collinear points"), - } - } - - #[test] - fn test_build_initial_simplex_rejects_coplanar_3d() { - // Coplanar points should be rejected by build_initial_simplex - 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.5, 0.5, 0.0]), - ]; - - let result = Triangulation::, (), (), 3>::build_initial_simplex(&vertices); - - assert!(result.is_err(), "Coplanar points should be rejected"); - match result { - Err(TriangulationConstructionError::GeometricDegeneracy { message }) => { - assert!( - message.contains("Degenerate") || message.contains("coplanar"), - "Error message should mention degeneracy or coplanarity" - ); - } - _ => panic!("Expected GeometricDegeneracy error for coplanar points"), - } - } - - #[test] - fn test_is_valid_rejects_negative_geometric_simplex_orientation() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let mut tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - - let simplex_key = tds.simplex_keys().next().unwrap(); - let simplex = tds.simplex_mut(simplex_key).unwrap(); - simplex.swap_vertex_slots(0, 1); - - let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let err = tri.is_valid().unwrap_err(); - assert!(matches!( - err, - InvariantError::Tds(TdsError::Geometric(GeometricError::NegativeOrientation { message })) - if message.contains("negative geometric orientation") - )); - } - - /// Calls `validate_geometric_simplex_orientation()` directly (not through `is_valid()` - /// which may short-circuit on coherent orientation checks) and asserts the returned - /// error contains vertex keys for debuggability. - #[test] - fn test_validate_geometric_simplex_orientation_returns_enriched_error_on_negative() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let mut tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - - let simplex_key = tds.simplex_keys().next().unwrap(); - tds.simplex_mut(simplex_key) - .unwrap() - .swap_vertex_slots(0, 1); - - let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let err = tri.validate_geometric_simplex_orientation().unwrap_err(); - assert!( - matches!( - &err, - TdsError::Geometric(GeometricError::NegativeOrientation { message }) - if message.contains("negative geometric orientation") - && message.contains("vertices") - ), - "Error should contain vertex keys: {err}" - ); - } - - #[test] - fn test_simplices_require_positive_orientation_promotion_detects_negative_without_mutating() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let mut tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let simplex_key = tds.simplex_keys().next().unwrap(); - tds.simplex_mut(simplex_key) - .unwrap() - .swap_vertex_slots(0, 1); - - let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let before: Vec<_> = tri.tds.simplex(simplex_key).unwrap().vertices().to_vec(); - - assert!( - tri.simplices_require_positive_orientation_promotion() - .unwrap(), - "Negative orientation should be detected" - ); - - let after: Vec<_> = tri.tds.simplex(simplex_key).unwrap().vertices().to_vec(); - assert_eq!( - before, after, - "Convergence check must not mutate simplex slot ordering" - ); - } - - #[test] - fn test_simplices_require_positive_orientation_promotion_false_for_positive_without_mutating() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let simplex_key = tds.simplex_keys().next().unwrap(); - - let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let before: Vec<_> = tri.tds.simplex(simplex_key).unwrap().vertices().to_vec(); - - assert!( - !tri.simplices_require_positive_orientation_promotion() - .unwrap(), - "Already-positive orientation should not require promotion" - ); - - let after: Vec<_> = tri.tds.simplex(simplex_key).unwrap().vertices().to_vec(); - assert_eq!( - before, after, - "Convergence check must not mutate already-positive simplices" - ); - } - - #[test] - fn test_periodic_geometric_orientation_validation_uses_lifted_coordinates() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([0.8, 0.0]), - vertex!([0.0, 0.8]), - ]; - let mut tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let simplex_key = tds.simplex_keys().next().unwrap(); - { - let simplex = tds.simplex_mut(simplex_key).unwrap(); - simplex - .set_periodic_vertex_offsets(vec![[0, 0], [0, 0], [1, 0]]) - .unwrap(); - } - - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - tri.set_global_topology(GlobalTopology::Toroidal { - domain: [1.0, 1.0], - mode: ToroidalConstructionMode::PeriodicImagePoint, - }); - - // In lifted coordinates this simplex is positively oriented. - assert!(tri.validate_geometric_simplex_orientation().is_ok()); - - // Flipping two slots should invert lifted orientation and be rejected. - { - let simplex = tri.tds.simplex_mut(simplex_key).unwrap(); - simplex.swap_vertex_slots(0, 1); - } - let err = tri.validate_geometric_simplex_orientation().unwrap_err(); - assert!(matches!( - err, - TdsError::Geometric(GeometricError::NegativeOrientation { message }) - if message.contains("negative geometric orientation") - )); - } - - #[test] - fn test_periodic_geometric_orientation_validation_requires_toroidal_metadata() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([0.8, 0.0]), - vertex!([0.0, 0.8]), - ]; - let mut tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let simplex_key = tds.simplex_keys().next().unwrap(); - { - let simplex = tds.simplex_mut(simplex_key).unwrap(); - simplex - .set_periodic_vertex_offsets(vec![[0, 0], [0, 0], [1, 0]]) - .unwrap(); - } - - let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let err = tri.validate_geometric_simplex_orientation().unwrap_err(); - assert!(matches!( - err, - TdsError::InconsistentDataStructure { message } - if message.contains("has periodic offsets") - && message.contains("expected periodic-orientation-offset-capable topology") - )); - } - - #[test] - fn test_periodic_geometric_orientation_validation_rejects_offset_count_mismatch() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([0.8, 0.0]), - vertex!([0.0, 0.8]), - ]; - let mut tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let simplex_key = tds.simplex_keys().next().unwrap(); - tds.simplex_mut(simplex_key) - .unwrap() - .periodic_vertex_offsets = Some(vec![[0, 0], [1, 0]].into()); - - let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let err = tri.validate_geometric_simplex_orientation().unwrap_err(); - assert!(matches!( - err, - TdsError::DimensionMismatch { - expected: 3, - actual: 2, - .. - } - )); - } - - #[test] - fn test_periodic_geometric_orientation_validation_maps_lift_errors() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([0.8, 0.0]), - vertex!([0.0, 0.8]), - ]; - let mut tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let simplex_key = tds.simplex_keys().next().unwrap(); - tds.simplex_mut(simplex_key) - .unwrap() - .set_periodic_vertex_offsets(vec![[0, 0], [0, 0], [1, 0]]) - .unwrap(); - - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - tri.set_global_topology(GlobalTopology::Toroidal { - domain: [0.0, 1.0], - mode: ToroidalConstructionMode::PeriodicImagePoint, - }); - - let err = tri.validate_geometric_simplex_orientation().unwrap_err(); - assert!(matches!( - err, - TdsError::InconsistentDataStructure { message } - if message.contains("Failed to lift coordinates") - && message.contains("Invalid toroidal period") - )); - } - - /// Consolidated macro for facet validation tests across dimensions. - /// - /// Verifies the manifold topology invariant: each facet shared by at most 2 simplices. - /// Consolidates detection and repair tests into comprehensive suites. - macro_rules! test_facet_validation { - ($dim:expr, [$($simplex_coords:expr),+ $(,)?]) => { - pastey::paste! { - #[test] - fn []() { - let vertices: Vec> = vec![ - $(vertex!($simplex_coords)),+ - ]; - - let tds = Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) - .unwrap(); - let tri = Triangulation::, (), (), $dim>::new_with_tds(FastKernel::new(), tds); - - // Valid simplex: should have no issues - let simplex_keys: Vec<_> = tri.tds.simplex_keys().collect(); - assert_eq!(simplex_keys.len(), 1); - let issues = tri.detect_local_facet_issues(&simplex_keys).unwrap(); - assert!(issues.is_none(), "{}D: Valid simplex should have no facet issues", $dim); - - // Empty list: should return None - let issues = tri.detect_local_facet_issues(&[]).unwrap(); - assert!(issues.is_none(), "{}D: Empty list should have no issues", $dim); - - // Nonexistent simplices: should be skipped gracefully - let fake_keys = vec![SimplexKey::default()]; - let issues = tri.detect_local_facet_issues(&fake_keys).unwrap(); - assert!(issues.is_none(), "{}D: Nonexistent simplices should be skipped", $dim); - - // Verify neighbors (all should be explicit boundary slots for a single simplex) - let (_, simplex) = tri.tds.simplices().next().unwrap(); - let neighbors = simplex - .neighbor_slots() - .expect("single simplex should assign boundary neighbor slots"); - assert!( - neighbors.iter().all(|slot| *slot == NeighborSlot::Boundary), - "{}D: Single simplex should have boundary slots", - $dim - ); - } - - #[test] - fn []() { - let vertices: Vec> = vec![ - $(vertex!($simplex_coords)),+ - ]; - - let tds = Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) - .unwrap(); - let mut tri = Triangulation::, (), (), $dim>::new_with_tds(FastKernel::new(), tds); - - // Empty issues map: should remove nothing - let empty_issues = FacetIssuesMap::default(); - let removed = tri.repair_local_facet_issues(&empty_issues).unwrap(); - assert_eq!(removed, 0, "{}D: Empty issues should remove 0 simplices", $dim); - assert_eq!(tri.tds.number_of_simplices(), 1, "{}D: Should still have 1 simplex", $dim); - } - } - }; - } - - /// Dimension-parametric `remove_vertex` tests. - /// - /// Verifies that vertex removal maintains neighbor pointer integrity and - /// triangulation validity across dimensions. - macro_rules! test_remove_vertex { - ($dim:expr, [$($simplex_coords:expr),+ $(,)?], $interior_point:expr) => { - pastey::paste! { - #[test] - fn []() { - // Build triangulation with D+1 simplex vertices + 1 interior point - let vertices: Vec> = { - let mut v = vec![$(vertex!($simplex_coords)),+]; - v.push(vertex!($interior_point)); - v - }; - - let mut dt = DelaunayTriangulation::new(&vertices) - .expect("Failed to create triangulation"); - - // Find and remove the interior vertex - let interior_vertex_key = dt - .vertices() - .find(|(_, v)| { - let coords = v.point().coords(); - coords.iter() - .zip($interior_point.iter()) - .all(|(a, b)| (a - b).abs() < 1e-10) - }) - .map(|(k, _)| k) - .expect("Interior vertex not found"); - - let initial_simplex_count = dt.tds().number_of_simplices(); - dt.remove_vertex(interior_vertex_key) - .expect("Failed to remove vertex"); - - // After removal, should have fewer simplices (or same if just 1 simplex left) - assert!(dt.tds().number_of_simplices() <= initial_simplex_count, - "{}D: Simplex count should not increase after removal", $dim); - - // Verify neighbor pointer consistency: - // 1. No dangling pointers (all neighbor keys exist) - // 2. Neighbor relationships are symmetric - for (simplex_key, simplex) in dt.tds().simplices() { - if let Some(neighbors) = simplex.neighbors() { - for (facet_idx, neighbor_opt) in neighbors.enumerate() { - if let Some(neighbor_key) = neighbor_opt { - // Verify neighbor exists - assert!( - dt.tds().contains_simplex(neighbor_key), - "{}D: Simplex {simplex_key:?} has neighbor pointer to non-existent simplex {neighbor_key:?}", - $dim - ); - - // Verify symmetry: neighbor should point back to us - let neighbor_simplex = dt - .tds() - .simplex(neighbor_key) - .expect("Neighbor simplex should exist"); - if let Some(mut neighbor_neighbors) = neighbor_simplex.neighbors() { - let points_back = neighbor_neighbors - .any(|neighbor| neighbor == Some(simplex_key)); - assert!( - points_back, - "{}D: Simplex {simplex_key:?} has neighbor {neighbor_key:?} at facet {facet_idx}, but neighbor doesn't point back", - $dim - ); - } - } - } - } - } - - // Verify triangulation is still valid (Levels 1–3; removal does not guarantee Delaunay) - let validation = dt.as_triangulation().validate(); - assert!( - validation.is_ok(), - "{}D: Triangulation should be structurally valid after vertex removal: {:?}", - $dim, - validation.err() - ); - } - } - }; - } - - /// Basic accessor tests across dimensions. - macro_rules! test_basic_accessors { - ($dim:expr, [$($simplex_coords:expr),+ $(,)?]) => { - pastey::paste! { - #[test] - fn []() { - // Empty triangulation - let empty: Triangulation, (), (), $dim> = - Triangulation::new_empty(FastKernel::new()); - assert_eq!(empty.number_of_vertices(), 0); - assert_eq!(empty.number_of_simplices(), 0); - assert_eq!(empty.dim(), -1); - assert_eq!(empty.simplices().count(), 0); - assert_eq!(empty.vertices().count(), 0); - assert_eq!(empty.facets().count(), 0); - assert_eq!(empty.boundary_facets().count(), 0); - - // Simplex triangulation - let vertices: Vec> = vec![ - $(vertex!($simplex_coords)),+ - ]; - let expected_vertex_count = vertices.len(); - - let tds = Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) - .unwrap(); - let tri = Triangulation::, (), (), $dim>::new_with_tds( - FastKernel::new(), - tds, - ); - - assert_eq!(tri.number_of_vertices(), expected_vertex_count); - assert_eq!(tri.number_of_simplices(), 1); - assert_eq!(tri.dim(), $dim as i32); - assert_eq!(tri.simplices().count(), 1); - assert_eq!(tri.vertices().count(), expected_vertex_count); - - // D-simplex has D+1 facets, all on boundary - let facet_count = tri.facets().count(); - assert_eq!(facet_count, expected_vertex_count, "{}D: D-simplex should have D+1 facets", $dim); - let boundary_count = tri.boundary_facets().count(); - assert_eq!(boundary_count, expected_vertex_count, "{}D: All facets should be on boundary", $dim); - } - } - }; - } - - // Facet validation tests (2D - 5D) - test_facet_validation!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); - test_facet_validation!( - 3, - [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0] - ] - ); - test_facet_validation!( - 4, - [ - [0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0] - ] - ); - test_facet_validation!( - 5, - [ - [0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0] - ] - ); - - // Basic accessor tests (2D - 5D) - test_basic_accessors!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); - test_basic_accessors!( - 3, - [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0] - ] - ); - test_basic_accessors!( - 4, - [ - [0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0] - ] - ); - test_basic_accessors!( - 5, - [ - [0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0] - ] - ); - - // Remove vertex tests (2D - 5D) - test_remove_vertex!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], [0.3, 0.3]); - test_remove_vertex!( - 3, - [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0] - ], - [0.25, 0.25, 0.25] - ); - test_remove_vertex!( - 4, - [ - [0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0] - ], - [0.2, 0.2, 0.2, 0.2] - ); - test_remove_vertex!( - 5, - [ - [0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0] - ], - [0.16, 0.16, 0.16, 0.16, 0.16] - ); - - // ============================================================================= - // Public Topology Traversal & Adjacency API (Read-only) - // ============================================================================= - - #[test] - fn test_topology_edges_triangle_2d() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - - let dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - let tri = dt.as_triangulation(); - - assert_eq!(tri.number_of_simplices(), 1); - assert_eq!(tri.number_of_vertices(), 3); - assert_eq!(tri.number_of_edges(), 3); - - let edges: HashSet<_> = tri.edges().collect(); - assert_eq!(edges.len(), 3); - - let index = tri.build_adjacency_index().unwrap(); - let edges_with_index: HashSet<_> = tri.edges_with_index(&index).collect(); - assert_eq!(edges_with_index, edges); - assert_eq!(tri.number_of_edges_with_index(&index), 3); - - // Edge endpoints should always be vertex keys from this triangulation. - assert!(edges.iter().all(|e| { - let (a, b) = e.endpoints(); - a != b && tri.vertex_coords(a).is_some() && tri.vertex_coords(b).is_some() - })); - } - - #[test] - fn test_topology_edges_and_incident_edges_double_tetrahedron_3d() { - // Two tetrahedra sharing a triangular facet. - let vertices: Vec<_> = vec![ - // Shared triangle - vertex!([0.0, 0.0, 0.0]), - vertex!([2.0, 0.0, 0.0]), - vertex!([1.0, 2.0, 0.0]), - // Two apices - vertex!([1.0, 0.7, 1.5]), - vertex!([1.0, 0.7, -1.5]), - ]; - - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - let tri = dt.as_triangulation(); - - assert_eq!(tri.number_of_simplices(), 2); - assert_eq!(tri.number_of_vertices(), 5); - - // This configuration has 9 unique edges (3 base + 6 apex-to-base). - assert_eq!(tri.number_of_edges(), 9); - - // A base vertex has degree 4: two base edges + two apex edges. - let base_vertex_key = tri - .vertices() - .find_map(|(vk, _)| (tri.vertex_coords(vk)? == [0.0, 0.0, 0.0]).then_some(vk)) - .unwrap(); - assert_eq!(tri.number_of_incident_edges(base_vertex_key), 4); - - let index = tri.build_adjacency_index().unwrap(); - assert_eq!(tri.number_of_edges_with_index(&index), 9); - - // A base vertex is incident to both simplices. - assert_eq!(tri.adjacent_simplices(base_vertex_key).count(), 2); - assert_eq!( - tri.adjacent_simplices_with_index(&index, base_vertex_key) - .count(), - 2 - ); - assert_eq!( - tri.number_of_adjacent_simplices_with_index(&index, base_vertex_key), - 2 - ); - - // A base vertex has degree 4: two base edges + two apex edges. - assert_eq!(tri.number_of_incident_edges(base_vertex_key), 4); - assert_eq!( - tri.incident_edges_with_index(&index, base_vertex_key) - .count(), - 4 - ); - assert_eq!( - tri.number_of_incident_edges_with_index(&index, base_vertex_key), - 4 - ); - - // An apex has degree 3: connected to all three base vertices. - let apex_vertex_key = tri - .vertices() - .find_map(|(vk, _)| (tri.vertex_coords(vk)? == [1.0, 0.7, 1.5]).then_some(vk)) - .unwrap(); - assert_eq!(tri.number_of_incident_edges(apex_vertex_key), 3); - assert_eq!( - tri.adjacent_simplices_with_index(&index, apex_vertex_key) - .count(), - 1 - ); - assert_eq!( - tri.number_of_adjacent_simplices_with_index(&index, apex_vertex_key), - 1 - ); - - // Each simplex has exactly one neighbor in the index. - let simplex_keys: Vec<_> = tri.simplices().map(|(ck, _)| ck).collect(); - for &ck in &simplex_keys { - assert_eq!(tri.simplex_neighbors_with_index(&index, ck).count(), 1); - assert_eq!(tri.number_of_simplex_neighbors_with_index(&index, ck), 1); - } - } - - #[test] - fn test_topology_queries_missing_keys_are_empty_or_none() { - // Use a "null" SlotMap key, which should never be present in a valid triangulation. - let vertices_a = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let dt_a: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices_a).unwrap(); - let tri_a = dt_a.as_triangulation(); - - let index = tri_a.build_adjacency_index().unwrap(); - - let missing_vertex_key = VertexKey::default(); - assert_eq!(tri_a.adjacent_simplices(missing_vertex_key).count(), 0); - assert_eq!( - tri_a - .adjacent_simplices_with_index(&index, missing_vertex_key) - .count(), - 0 - ); - assert_eq!( - tri_a.number_of_adjacent_simplices_with_index(&index, missing_vertex_key), - 0 - ); - - assert_eq!(tri_a.incident_edges(missing_vertex_key).count(), 0); - assert_eq!( - tri_a - .incident_edges_with_index(&index, missing_vertex_key) - .count(), - 0 - ); - assert_eq!(tri_a.number_of_incident_edges(missing_vertex_key), 0); - assert_eq!( - tri_a.number_of_incident_edges_with_index(&index, missing_vertex_key), - 0 - ); - assert!(tri_a.vertex_coords(missing_vertex_key).is_none()); - - let missing_simplex_key = SimplexKey::default(); - assert_eq!(tri_a.simplex_neighbors(missing_simplex_key).count(), 0); - assert_eq!( - tri_a - .simplex_neighbors_with_index(&index, missing_simplex_key) - .count(), - 0 - ); - assert_eq!( - tri_a.number_of_simplex_neighbors_with_index(&index, missing_simplex_key), - 0 - ); - assert!(tri_a.simplex_vertices(missing_simplex_key).is_none()); - } - - #[test] - fn test_topology_geometry_accessors_roundtrip() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - - let dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - let tri = dt.as_triangulation(); - - let v_key = tri - .vertices() - .find_map(|(vk, _)| (tri.vertex_coords(vk)? == [1.0, 0.0]).then_some(vk)) - .unwrap(); - assert_eq!(tri.vertex_coords(v_key).unwrap(), [1.0, 0.0]); - - let simplex_key = tri.simplices().next().unwrap().0; - let simplex_vertices = tri.simplex_vertices(simplex_key).unwrap(); - assert_eq!(simplex_vertices.len(), 3); - assert!(simplex_vertices.contains(&v_key)); - } - - #[test] - fn test_build_adjacency_index_basic_invariants() { - // Two tetrahedra sharing a triangular facet. - let vertices: Vec<_> = vec![ - // Shared triangle - vertex!([0.0, 0.0, 0.0]), - vertex!([2.0, 0.0, 0.0]), - vertex!([1.0, 2.0, 0.0]), - // Two apices - vertex!([1.0, 0.7, 1.5]), - vertex!([1.0, 0.7, -1.5]), - ]; - - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - let tri = dt.as_triangulation(); - - let index = tri.build_adjacency_index().unwrap(); - - // Each simplex has exactly one neighbor. - let simplex_keys: Vec<_> = tri.simplices().map(|(ck, _)| ck).collect(); - assert_eq!(simplex_keys.len(), 2); - for &ck in &simplex_keys { - let neighbors = index.simplex_to_neighbors.get(&ck).unwrap(); - assert_eq!(neighbors.len(), 1); - assert!(simplex_keys.contains(&neighbors[0])); - assert_ne!(neighbors[0], ck); - } - - // For every vertex, edges/simplices lists exist and are consistent. - for (vk, _) in tri.vertices() { - let simplices = index.vertex_to_simplices.get(&vk).unwrap(); - assert!(!simplices.is_empty()); - - let edges = index.vertex_to_edges.get(&vk).unwrap(); - assert!(!edges.is_empty()); - assert!( - edges - .iter() - .all(|e| matches!(e.endpoints(), (a, b) if a == vk || b == vk)) - ); - } - } - - #[test] - fn test_build_adjacency_index_empty_triangulation_is_empty() { - let tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - let index = tri.build_adjacency_index().unwrap(); - assert!(index.vertex_to_simplices.is_empty()); - assert!(index.simplex_to_neighbors.is_empty()); - assert!(index.vertex_to_edges.is_empty()); - } - - #[test] - fn test_build_adjacency_index_includes_isolated_vertex_entries() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - let isolated_vertex = tri - .tds - .insert_vertex_with_mapping(vertex!([10.0, 10.0])) - .unwrap(); - let index = tri.build_adjacency_index().unwrap(); - - assert!( - index - .vertex_to_simplices - .get(&isolated_vertex) - .is_some_and(SmallBuffer::is_empty) - ); - assert!( - index - .vertex_to_edges - .get(&isolated_vertex) - .is_some_and(SmallBuffer::is_empty) - ); - } - - #[test] - fn test_build_adjacency_index_errors_on_missing_neighbor_simplex() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let simplex_key = tri.tds.simplex_keys().next().unwrap(); - - let mut missing_neighbor = SimplexKey::default(); - if tri.tds.contains_simplex(missing_neighbor) { - missing_neighbor = SimplexKey::from(KeyData::from_ffi(u64::MAX)); - } - assert!(!tri.tds.contains_simplex(missing_neighbor)); - - { - let simplex = tri.tds.simplex_mut(simplex_key).unwrap(); - simplex - .set_neighbors_from_keys([Some(missing_neighbor), None, None]) - .unwrap(); - } - - match tri.build_adjacency_index() { - Err(AdjacencyIndexBuildError::MissingNeighborSimplex { - simplex_key: err_simplex_key, - neighbor_key, - }) => { - assert_eq!(err_simplex_key, simplex_key); - assert_eq!(neighbor_key, missing_neighbor); - } - other => panic!("Expected MissingNeighborSimplex, got {other:?}"), - } - } - - #[test] - fn test_simplex_neighbors_filters_missing_neighbor_simplex() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let simplex_key = tri.tds.simplex_keys().next().unwrap(); - let missing_neighbor = SimplexKey::from(KeyData::from_ffi(u64::MAX)); - assert!(!tri.tds.contains_simplex(missing_neighbor)); - - tri.tds - .simplex_mut(simplex_key) - .unwrap() - .set_neighbors_from_keys([Some(missing_neighbor), None, None]) - .unwrap(); - - assert_eq!(tri.simplex_neighbors(simplex_key).count(), 0); - } - - #[test] - fn test_build_adjacency_index_errors_on_missing_vertex_key() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let simplex_key = tri.tds.simplex_keys().next().unwrap(); - let existing_vertices = tri.tds.simplex(simplex_key).unwrap().vertices().to_vec(); - - let mut missing_vertex = VertexKey::default(); - if tri.tds.contains_vertex_key(missing_vertex) { - missing_vertex = VertexKey::from(KeyData::from_ffi(u64::MAX)); - } - assert!(!tri.tds.contains_vertex_key(missing_vertex)); - - { - let simplex = tri.tds.simplex_mut(simplex_key).unwrap(); - simplex.clear_vertex_keys(); - simplex.push_vertex_key(existing_vertices[0]); - simplex.push_vertex_key(existing_vertices[1]); - simplex.push_vertex_key(missing_vertex); - } - - match tri.build_adjacency_index() { - Err(AdjacencyIndexBuildError::MissingVertexKey { - simplex_key: err_simplex_key, - vertex_key, - }) => { - assert_eq!(err_simplex_key, simplex_key); - assert_eq!(vertex_key, missing_vertex); - } - other => panic!("Expected MissingVertexKey, got {other:?}"), - } - } - - // ============================================================================= - // Triangulation insert_with_statistics tests (internal API) - // ============================================================================= - - #[test] - fn triangulation_insert_with_statistics_basic_2d() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - // Insert first vertex - let (outcome, stats) = tri - .insert_with_statistics(vertex!([0.0, 0.0]), None, None) - .expect("insertion should succeed"); - - assert!(matches!( - outcome, - InsertionOutcome::Inserted { hint: None, .. } - )); - assert_eq!(stats.attempts, 1); - assert!(!stats.used_perturbation()); - assert!(!stats.skipped()); - assert!(stats.success()); - assert_eq!(tri.number_of_vertices(), 1); - } - - #[test] - fn triangulation_insert_with_statistics_bootstrap_3d() { - let mut tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - // Insert D+1 vertices to create initial simplex - 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]), - ]; - - for (i, v) in vertices.into_iter().enumerate() { - let (outcome, stats) = tri.insert_with_statistics(v, None, None).unwrap(); - - assert!(matches!(outcome, InsertionOutcome::Inserted { .. })); - assert_eq!(stats.attempts, 1); - - if i < 3 { - // Bootstrap phase - no hint yet - assert!(matches!( - outcome, - InsertionOutcome::Inserted { hint: None, .. } - )); - } else { - // After D+1 vertices, hint should be available - assert!(matches!( - outcome, - InsertionOutcome::Inserted { hint: Some(_), .. } - )); - } - } - - assert_eq!(tri.number_of_vertices(), 4); - assert_eq!(tri.number_of_simplices(), 1); - } - - #[test] - fn triangulation_exterior_insert_3d_uses_local_conflict_without_global_scan() { - let mut tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - for coords in [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0], - ] { - tri.insert_with_statistics(vertex!(coords), None, None) - .unwrap(); - } - - let hint = tri.simplices().next().map(|(simplex_key, _)| simplex_key); - let detail = tri - .insert_with_statistics_seeded_indexed_detailed( - vertex!([2.0, 2.0, 2.0]), - None, - hint, - 0, - None, - None, - ) - .unwrap(); - - assert!(matches!(detail.outcome, InsertionOutcome::Inserted { .. })); - assert_eq!(detail.telemetry.global_conflict_scans, 0); - assert_eq!(detail.telemetry.global_conflict_simplices_scanned, 0); - assert_eq!(detail.telemetry.global_conflict_simplices_found_total, 0); - assert_eq!(detail.telemetry.global_conflict_scan_nanos, 0); - assert_eq!(detail.telemetry.conflict_region_calls, 1); - assert_eq!(detail.telemetry.conflict_region_simplices_total, 0); - assert_eq!(detail.telemetry.cavity_insertion_calls, 0); - assert_eq!(detail.telemetry.hull_extension_calls, 1); - assert!( - !detail.repair_seed_simplices.is_empty(), - "hull extension should return local repair seeds" - ); - let facet_to_simplices = tri.tds.build_facet_to_simplices_map().unwrap(); - assert!( - facet_to_simplices - .values() - .all(|incident_simplices| incident_simplices.len() <= 2), - "hull extension should leave every facet with at most two incident simplices" - ); - assert!(tri.is_valid().is_ok()); - } - - #[test] - fn triangulation_exterior_insert_with_empty_conflicts_uses_local_repair_seeds() { - let mut tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - for coords in [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0], - ] { - tri.insert_with_statistics(vertex!(coords), None, None) - .unwrap(); - } - - let hint = tri.simplices().next().map(|(simplex_key, _)| simplex_key); - let empty_conflicts = SimplexKeyBuffer::new(); - let detail = tri - .insert_with_statistics_seeded_indexed_detailed( - vertex!([2.0, 2.0, 2.0]), - Some(&empty_conflicts), - hint, - 0, - None, - None, - ) - .unwrap(); - - assert!(matches!(detail.outcome, InsertionOutcome::Inserted { .. })); - assert_eq!(detail.telemetry.global_conflict_scans, 0); - assert_eq!(detail.telemetry.conflict_region_calls, 1); - assert_eq!(detail.telemetry.conflict_region_simplices_total, 0); - assert_eq!(detail.telemetry.cavity_insertion_calls, 0); - assert_eq!(detail.telemetry.hull_extension_calls, 1); - assert!( - !detail.repair_seed_simplices.is_empty(), - "empty caller conflicts should still use terminal-simplex local repair seeds" - ); - let facet_to_simplices = tri.tds.build_facet_to_simplices_map().unwrap(); - assert!( - facet_to_simplices - .values() - .all(|incident_simplices| incident_simplices.len() <= 2), - "hull extension should leave every facet with at most two incident simplices" - ); - assert!(tri.is_valid().is_ok()); - } - - #[test] - fn triangulation_caller_conflicts_do_not_force_delaunay_repair() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - let start_simplex = tri - .simplices() - .next() - .map(|(simplex_key, _)| simplex_key) - .unwrap(); - let mut conflict_simplices = SimplexKeyBuffer::new(); - conflict_simplices.push(start_simplex); - let detail = tri - .insert_with_statistics_seeded_indexed_detailed( - vertex!([0.25, 0.25]), - Some(&conflict_simplices), - Some(start_simplex), - 0, - None, - None, - ) - .unwrap(); - - assert!(matches!(detail.outcome, InsertionOutcome::Inserted { .. })); - assert!( - !detail.delaunay_repair_required, - "caller-provided conflict simplices should preserve the cavity insertion repair flag" - ); - assert!(tri.is_valid().is_ok()); - } - - #[test] - fn triangulation_required_topology_validation_records_telemetry() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - tri.set_validation_policy(ValidationPolicy::OnSuspicion); - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - - let hint = tri.simplices().next().map(|(simplex_key, _)| simplex_key); - let detail = tri - .insert_with_statistics_seeded_indexed_detailed( - vertex!([0.25, 0.25]), - None, - hint, - 0, - None, - None, - ) - .unwrap(); - - assert!(matches!(detail.outcome, InsertionOutcome::Inserted { .. })); - assert!( - detail.telemetry.topology_validation_calls > 0, - "Pseudomanifold insertion should record required topology validation" - ); - assert_eq!( - detail.telemetry.topology_validation_nanos, 0, - "default detailed insertion should not start validation timers" - ); - - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - tri.set_validation_policy(ValidationPolicy::OnSuspicion); - tri.set_topology_guarantee(TopologyGuarantee::PLManifold); - - let hint = tri.simplices().next().map(|(simplex_key, _)| simplex_key); - let detail = tri - .insert_with_statistics_seeded_indexed_detailed( - vertex!([0.25, 0.25]), - None, - hint, - 0, - None, - None, - ) - .unwrap(); - - assert!(matches!(detail.outcome, InsertionOutcome::Inserted { .. })); - assert!( - detail.telemetry.topology_validation_calls > 0, - "PLManifold insertion should record RequiredTopologyLinks validation" - ); - assert_eq!( - detail.telemetry.topology_validation_nanos, 0, - "default detailed insertion should not start validation timers" - ); - } - - #[test] - fn triangulation_insert_with_statistics_hint_usage_4d() { - let mut tri: Triangulation, (), (), 4> = - Triangulation::new_empty(FastKernel::new()); - - // Build initial simplex - for i in 0..5 { - let mut coords = [0.0; 4]; - if i > 0 { - coords[i - 1] = 1.0; - } - tri.insert_with_statistics(vertex!(coords), None, None) - .unwrap(); - } - - // Insert with explicit hint - let hint_simplex = tri.simplices().next().map(|(key, _)| key); - let (outcome, stats) = tri - .insert_with_statistics(vertex!([0.2, 0.2, 0.2, 0.2]), None, hint_simplex) - .unwrap(); - - assert!(matches!( - outcome, - InsertionOutcome::Inserted { hint: Some(_), .. } - )); - assert_eq!(stats.attempts, 1); - assert!(stats.success()); - } - - #[test] - fn triangulation_insert_with_statistics_duplicate_coordinates_3d() { - let mut tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - // Insert first vertex - tri.insert_with_statistics(vertex!([1.0, 2.0, 3.0]), None, None) - .unwrap(); - - // Try duplicate - should be skipped - let result = tri.insert_with_statistics(vertex!([1.0, 2.0, 3.0]), None, None); - - assert!(matches!( - result, - Ok(( - InsertionOutcome::Skipped { - error: InsertionError::DuplicateCoordinates { .. } - }, - _ - )) - )); - } - - #[test] - fn triangulation_insert_with_statistics_multiple_insertions_2d() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - let points = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.5, 1.0]), - vertex!([0.3, 0.3]), - vertex!([0.7, 0.3]), - ]; - - let mut all_succeeded = true; - let mut max_attempts = 0; - - for point in points { - match tri.insert_with_statistics(point, None, None) { - Ok((InsertionOutcome::Inserted { .. }, stats)) => { - max_attempts = max_attempts.max(stats.attempts); - assert!(stats.success()); - } - Ok((InsertionOutcome::Skipped { .. }, _)) | Err(_) => { - all_succeeded = false; - } - } - } - - assert!(all_succeeded, "all insertions should succeed"); - assert!(max_attempts >= 1); - assert_eq!(tri.number_of_vertices(), 5); - } - - #[test] - fn triangulation_insert_with_statistics_outcome_types() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - // Test Inserted variant - let (outcome, _) = tri - .insert_with_statistics(vertex!([0.0, 0.0]), None, None) - .unwrap(); - - match outcome { - InsertionOutcome::Inserted { vertex_key, hint } => { - // Verify we can access the fields - assert!(tri.vertices().any(|(k, _)| k == vertex_key)); - assert_eq!(hint, None); // No hint during bootstrap - } - InsertionOutcome::Skipped { .. } => panic!("expected Inserted, got Skipped"), - } - } - - #[test] - fn triangulation_insert_with_statistics_sequential_5d() { - let mut tri: Triangulation, (), (), 5> = - Triangulation::new_empty(FastKernel::new()); - - // Insert 6 vertices to form initial simplex - for i in 0..6 { - let mut coords = [0.0; 5]; - if i > 0 { - coords[i - 1] = 1.0; - } - - let (outcome, stats) = tri - .insert_with_statistics(vertex!(coords), None, None) - .unwrap(); - - assert!(matches!(outcome, InsertionOutcome::Inserted { .. })); - assert_eq!(stats.attempts, 1); - assert!(stats.success()); - } - - assert_eq!(tri.number_of_vertices(), 6); - assert_eq!(tri.number_of_simplices(), 1); - } - - #[test] - fn statistics_simplices_removed_during_repair() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - // Build simplex - tri.insert_with_statistics(vertex!([0.0, 0.0]), None, None) - .unwrap(); - tri.insert_with_statistics(vertex!([1.0, 0.0]), None, None) - .unwrap(); - tri.insert_with_statistics(vertex!([0.5, 1.0]), None, None) - .unwrap(); - - let simplices_before = tri.number_of_simplices(); - - // Insert interior point - might trigger repair - let (_outcome, stats) = tri - .insert_with_statistics(vertex!([0.5, 0.3]), None, None) - .unwrap(); - - let simplices_after = tri.number_of_simplices(); - - // Basic sanity: repair can't remove more simplices than existed before insertion. - assert!( - stats.simplices_removed_during_repair <= simplices_before, - "simplices_removed_during_repair ({}) should not exceed simplex count before insertion ({}); simplices after insertion: {}", - stats.simplices_removed_during_repair, - simplices_before, - simplices_after - ); - } - - // ============================================================================= - // insert_with_conflict_region: cavity reduction loop branch coverage - // - // These tests exercise `insert_with_conflict_region` directly via a synthetic - // TDS rather than through the public API. The goal is to cover the loop arms - // (RidgeFan SHRINK, DisconnectedBoundary EXPAND / SHRINK-fallback / else-break, - // and the post-loop error paths) that are not reachable through normal Delaunay - // insertions. - // ============================================================================= - - /// `DisconnectedBoundary` where disconnected simplices have no non-conflict neighbours: - /// `else { break; }` fires, then the D<3 star-split fallback is taken. - /// - /// Covers: `DisconnectedBoundary` `else { break; }` (line 3492), `should_fallback=true` - /// path (lines 3530-3555), and `suspicion.fallback_star_split` being set. - #[test] - fn test_cavity_reduction_disconnected_no_neighbors_sets_star_split_2d() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - // Two triangles that share no vertices (→ DisconnectedBoundary on extraction). - let v0 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0])) - .unwrap(); - let v1 = tri - .tds - .insert_vertex_with_mapping(vertex!([1.0, 0.0])) - .unwrap(); - let v2 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.5, 1.0])) - .unwrap(); - let v3 = tri - .tds - .insert_vertex_with_mapping(vertex!([5.0, 0.0])) - .unwrap(); - let v4 = tri - .tds - .insert_vertex_with_mapping(vertex!([6.0, 0.0])) - .unwrap(); - let v5 = tri - .tds - .insert_vertex_with_mapping(vertex!([5.5, 1.0])) - .unwrap(); - - let simplex_a = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) - .unwrap(); - let simplex_b = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![v3, v4, v5], None).unwrap()) - .unwrap(); - - // Neither simplex has any neighbour pointers, so `simplices_to_add` will be empty on - // the first iteration and the `else { break; }` arm fires immediately. - let new_v = tri - .tds - .insert_vertex_with_mapping(vertex!([0.5, 0.3])) - .unwrap(); - let point = Point::new([0.5_f64, 0.3_f64]); - - let mut conflict_simplices = SimplexKeyBuffer::new(); - conflict_simplices.push(simplex_a); - conflict_simplices.push(simplex_b); - - let mut suspicion = SuspicionFlags::default(); - let _ = tri.insert_with_conflict_region( - new_v, - &point, - conflict_simplices, - Some(simplex_a), - &mut suspicion, - ); - - // `else { break; }` → Err(DisconnectedBoundary) → should_fallback=true (D<3) - // → star-split fallback sets suspicion.fallback_star_split. - assert!( - suspicion.fallback_star_split, - "DisconnectedBoundary with no non-conflict neighbours should trigger star-split (D=2)" - ); - } - - /// Three 3D tetrahedra sharing the same triangular face → `NonManifoldFacet` on the - /// first extraction. D=3 → `should_fallback=false` → the function returns Err - /// immediately without entering the star-split path. - /// - /// Covers: `_ => break` (line 3511), `should_fallback=false` path (lines 3558-3563). - #[test] - fn test_cavity_reduction_nonmanifold_3d_returns_error_without_star_split() { - let mut tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - - let v0 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) - .unwrap(); - let v1 = tri - .tds - .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) - .unwrap(); - let v2 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) - .unwrap(); - // Three distinct fourth vertices that all pair with the {v0,v1,v2} face. - let v3 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) - .unwrap(); - let v4 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, -1.0])) - .unwrap(); - let v5 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0, 2.0])) - .unwrap(); - - let simplex1 = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap()) - .unwrap(); - let simplex2 = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v4], None).unwrap()) - .unwrap(); - let simplex3 = tri - .tds - .insert_simplex_bypassing_topology_checks_for_test( - Simplex::new(vec![v0, v1, v2, v5], None).unwrap(), - ) - .unwrap(); - - let new_v = tri - .tds - .insert_vertex_with_mapping(vertex!([0.1, 0.1, 0.1])) - .unwrap(); - let point = Point::new([0.1_f64, 0.1_f64, 0.1_f64]); - - let mut conflict_simplices = SimplexKeyBuffer::new(); - conflict_simplices.push(simplex1); - conflict_simplices.push(simplex2); - conflict_simplices.push(simplex3); - - let mut suspicion = SuspicionFlags::default(); - let result = tri.insert_with_conflict_region( - new_v, - &point, - conflict_simplices, - None, - &mut suspicion, - ); - - // NonManifoldFacet → `_ => break` → should_fallback = D<3 = false → Err returned. - assert!(result.is_err(), "D=3 NonManifoldFacet should return Err"); - assert!( - !suspicion.fallback_star_split, - "D=3 should NOT enter star-split fallback" - ); - } - - /// Four 2D triangles all sharing a common vertex but with no shared edges produce a - /// `RidgeFan` error (`facet_count >= 3` for the shared vertex). Because - /// `conflict_simplices.len() = 4 > D+1 = 3`, the SHRINK branch fires on the first - /// iteration, removing the extra fan simplices from the conflict region. - /// - /// Covers: `RidgeFan` SHRINK body (lines 3434-3442) and re-extraction (line 3514). - #[test] - fn test_cavity_reduction_ridge_fan_shrink_fires_for_4_conflict_simplices_2d() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - // `center` appears in 8 boundary edges (2 per simplex × 4 simplices) → RidgeFan. - let center = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0])) - .unwrap(); - let va = tri - .tds - .insert_vertex_with_mapping(vertex!([-1.0, 2.0])) - .unwrap(); - let vb = tri - .tds - .insert_vertex_with_mapping(vertex!([1.0, 2.0])) - .unwrap(); - let vc = tri - .tds - .insert_vertex_with_mapping(vertex!([-3.0, -2.0])) - .unwrap(); - let vd = tri - .tds - .insert_vertex_with_mapping(vertex!([-2.0, -3.0])) - .unwrap(); - let ve = tri - .tds - .insert_vertex_with_mapping(vertex!([3.0, -2.0])) - .unwrap(); - let vf = tri - .tds - .insert_vertex_with_mapping(vertex!([2.0, -3.0])) - .unwrap(); - let vg = tri - .tds - .insert_vertex_with_mapping(vertex!([-4.0, 1.0])) - .unwrap(); - let vh = tri - .tds - .insert_vertex_with_mapping(vertex!([-4.0, -1.0])) - .unwrap(); - - let simplex1 = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![center, va, vb], None).unwrap()) - .unwrap(); - let simplex2 = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![center, vc, vd], None).unwrap()) - .unwrap(); - let simplex3 = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![center, ve, vf], None).unwrap()) - .unwrap(); - let simplex4 = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![center, vg, vh], None).unwrap()) - .unwrap(); - - let new_v = tri - .tds - .insert_vertex_with_mapping(vertex!([0.3, 1.0])) - .unwrap(); - let point = Point::new([0.3_f64, 1.0_f64]); - - let mut conflict_simplices = SimplexKeyBuffer::new(); - conflict_simplices.push(simplex1); - conflict_simplices.push(simplex2); - conflict_simplices.push(simplex3); - conflict_simplices.push(simplex4); - - let mut suspicion = SuspicionFlags::default(); - // RidgeFan SHRINK fires on iteration 1 (4 > D+1=3), reducing conflict_simplices. - // The function completes without panic; result may be Ok or Err. - let _ = tri.insert_with_conflict_region( - new_v, - &point, - conflict_simplices, - Some(simplex1), - &mut suspicion, - ); - // Reaching here confirms the SHRINK branch executed successfully. - } - - /// Two completely disconnected 2D conflict simplices that each have one non-conflict - /// neighbour trigger the `DisconnectedBoundary` EXPAND path on the first iteration - /// (adding the neighbours), and the SHRINK-fallback on a subsequent iteration - /// (when `simplices_to_add` is empty but `conflict_simplices.len() > D+1`). - /// - /// Covers: EXPAND body (lines 3470-3480), SHRINK-fallback (lines 3481-3491), - /// and re-extraction after each reshape (line 3514). - #[test] - fn test_cavity_reduction_disconnected_expand_then_shrink_2d() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - // Group A: simplex_a = {v0,v1,v2} shares edge {v0,v1} with simplex_c = {v0,v1,v6}. - let v0 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.0, 0.0])) - .unwrap(); - let v1 = tri - .tds - .insert_vertex_with_mapping(vertex!([1.0, 0.0])) - .unwrap(); - let v2 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.5, 1.0])) - .unwrap(); - let v6 = tri - .tds - .insert_vertex_with_mapping(vertex!([0.5, -1.0])) - .unwrap(); - // Group B: simplex_b = {v3,v4,v5} shares edge {v3,v4} with simplex_d = {v3,v4,v7}. - let v3 = tri - .tds - .insert_vertex_with_mapping(vertex!([5.0, 0.0])) - .unwrap(); - let v4 = tri - .tds - .insert_vertex_with_mapping(vertex!([6.0, 0.0])) - .unwrap(); - let v5 = tri - .tds - .insert_vertex_with_mapping(vertex!([5.5, 1.0])) - .unwrap(); - let v7 = tri - .tds - .insert_vertex_with_mapping(vertex!([5.5, -1.0])) - .unwrap(); - - let simplex_a = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) - .unwrap(); - // simplex_c is a non-conflict neighbour of simplex_a (not initially in conflict_simplices). - let simplex_c = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v6], None).unwrap()) - .unwrap(); - let simplex_b = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![v3, v4, v5], None).unwrap()) - .unwrap(); - // simplex_d is a non-conflict neighbour of simplex_b. - let simplex_d = tri - .tds - .insert_simplex_with_mapping(Simplex::new(vec![v3, v4, v7], None).unwrap()) - .unwrap(); - - // Wire neighbours so EXPAND discovers simplex_c via simplex_a and simplex_d via simplex_b. - { - let simplex = tri.tds.simplex_mut(simplex_a).unwrap(); - simplex - .set_neighbors_from_keys([Some(simplex_c), None, None]) - .unwrap(); - } - { - let simplex = tri.tds.simplex_mut(simplex_b).unwrap(); - simplex - .set_neighbors_from_keys([Some(simplex_d), None, None]) - .unwrap(); - } - - let new_v = tri - .tds - .insert_vertex_with_mapping(vertex!([0.5, 0.3])) - .unwrap(); - let point = Point::new([0.5_f64, 0.3_f64]); - - let mut conflict_simplices = SimplexKeyBuffer::new(); - conflict_simplices.push(simplex_a); - conflict_simplices.push(simplex_b); - - // Iteration trace: - // 1. DisconnectedBoundary → EXPAND (adds simplex_c or simplex_d) → re-extract. - // 2. DisconnectedBoundary → EXPAND (adds the other) → re-extract. - // 3. DisconnectedBoundary, simplices_to_add=empty (all neighbours in conflict_set), - // len=4 > D+1=3 → SHRINK-fallback removes disconnected component → re-extract. - // 4. Two simplices sharing an edge → connected boundary → Ok → break. - let mut suspicion = SuspicionFlags::default(); - let _ = tri.insert_with_conflict_region( - new_v, - &point, - conflict_simplices, - Some(simplex_a), - &mut suspicion, - ); - // Reaching here without panic confirms EXPAND and SHRINK branches executed. - } - - // ---- insertion_error_to_invariant_error tests ---- - - #[test] - fn test_insertion_error_to_invariant_error_tds_arm() { - let source = TdsError::Geometric(GeometricError::DegenerateOrientation { - message: "det=0".to_string(), - }); - let error = InsertionError::TopologyValidation(source.clone()); - let result = insertion_error_to_invariant_error(error, "ctx"); - assert_eq!(result, InvariantError::Tds(source)); - } - - #[test] - fn test_insertion_error_to_invariant_error_triangulation_arm() { - let inner = TriangulationValidationError::IsolatedVertex { - vertex_key: VertexKey::from(KeyData::from_ffi(1)), - vertex_uuid: Uuid::nil(), - }; - let error = InsertionError::TopologyValidationFailed { - message: "outer".to_string(), - source: inner.clone(), - }; - let result = insertion_error_to_invariant_error(error, "ctx"); - assert_eq!(result, InvariantError::Triangulation(inner)); - } - - #[test] - fn test_insertion_error_to_invariant_error_other_arm() { - let error = InsertionError::CavityFilling { - reason: CavityFillingError::EmptyFanTriangulation, - }; - let result = insertion_error_to_invariant_error(error, "ctx"); - assert!( - matches!( - result, - InvariantError::Tds(TdsError::InconsistentDataStructure { ref message }) - if message.contains("ctx") && message.contains("fan triangulation produced no simplices") - ), - "CavityFilling should wrap to InconsistentDataStructure: {result:?}" - ); - } - - // ---- invariant_error_to_insertion_error coverage ---- - - #[test] - fn test_invariant_error_to_insertion_error_tds_arm() { - let inv = InvariantError::Tds(TdsError::InconsistentDataStructure { - message: "test".to_string(), - }); - let ins = - Triangulation::, (), (), 3>::invariant_error_to_insertion_error(inv); - assert!(matches!(ins, InsertionError::TopologyValidation(_))); - } - - #[test] - fn test_invariant_error_to_insertion_error_triangulation_arm() { - let inv = InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { - vertex_key: VertexKey::from(KeyData::from_ffi(1)), - vertex_uuid: Uuid::nil(), - }); - let ins = - Triangulation::, (), (), 3>::invariant_error_to_insertion_error(inv); - assert!(matches!( - ins, - InsertionError::TopologyValidationFailed { .. } - )); - } - - #[test] - fn test_invariant_error_to_insertion_error_delaunay_arm() { - let inv = - InvariantError::Delaunay(DelaunayTriangulationValidationError::VerificationFailed { - message: "test".to_string(), - }); - let ins = - Triangulation::, (), (), 3>::invariant_error_to_insertion_error(inv); - assert!(matches!( - ins, - InsertionError::DelaunayValidationFailed { .. } - )); - } - - #[test] - fn test_from_manifold_error_for_invariant_error_non_tds() { - let err = ManifoldError::ManifoldFacetMultiplicity { - facet_key: 999, - simplex_count: 5, - }; - let inv = InvariantError::from(err); - assert!(matches!( - inv, - InvariantError::Triangulation( - TriangulationValidationError::ManifoldFacetMultiplicity { - facet_key: 999, - simplex_count: 5 - } - ) - )); - } - - #[test] - fn test_triangulation_validation_error_isolated_vertex_display() { - let err = TriangulationValidationError::IsolatedVertex { - vertex_key: VertexKey::from(KeyData::from_ffi(42)), - vertex_uuid: Uuid::nil(), - }; - let msg = err.to_string(); - assert!(msg.contains("Isolated vertex")); - assert!(msg.contains("not incident to any simplex")); - } - - // ---- is_valid / validate error-path tests ---- - - #[test] - fn test_is_valid_returns_invariant_error_for_isolated_vertex() { - let (mut tri, _, _) = build_single_tet(); - - // Add an isolated vertex that is not referenced by any simplex. - let iso = tri - .tds - .insert_vertex_with_mapping(vertex!([0.5, 0.5, 0.5])) - .unwrap(); - - match tri.is_valid() { - Err(InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { - vertex_key, - .. - })) => { - assert_eq!(vertex_key, iso); - } - other => { - panic!("Expected InvariantError::Triangulation(IsolatedVertex), got {other:?}") - } - } - } - - #[test] - fn test_is_valid_returns_triangulation_error_for_disconnected() { - let tds = build_disconnected_two_triangles_tds_2d(); - let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - match tri.is_valid() { - Err(InvariantError::Triangulation(TriangulationValidationError::Disconnected { - simplex_count, - })) => { - assert_eq!( - simplex_count, 2, - "Expected 2 simplices in disconnected triangulation" - ); - } - other => { - panic!("Expected InvariantError::Triangulation(Disconnected), got {other:?}") - } - } - } - - #[test] - fn test_validate_returns_invariant_error_from_tds_layer() { - // Corrupt a TDS so that Level 2 structural validation fails. - let (mut tri, [v0, _, _, _], _) = build_single_tet(); - - // Break vertex mapping: remove uuid entry. - let uuid = tri.tds.vertex(v0).unwrap().uuid(); - tri.tds.uuid_to_vertex_key.remove(&uuid); - - match tri.validate() { - Err(InvariantError::Tds(TdsError::MappingInconsistency { .. })) => {} - other => panic!("Expected InvariantError::Tds(MappingInconsistency), got {other:?}"), - } - } - - #[test] - fn test_validate_returns_invariant_error_from_topology_layer() { - let (mut tri, _, _) = build_single_tet(); - - // Add an isolated vertex so Level 3 (topology) fails. - let _ = tri - .tds - .insert_vertex_with_mapping(vertex!([0.5, 0.5, 0.5])) - .unwrap(); - - match tri.validate() { - Err(InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { - .. - })) => {} - other => { - panic!("Expected InvariantError::Triangulation(IsolatedVertex), got {other:?}") - } - } - } - - #[test] - fn test_from_manifold_error_tds_routes_to_invariant_error_tds() { - let tds_err = TdsError::InconsistentDataStructure { - message: "underlying TDS issue".to_string(), - }; - let manifold_err = ManifoldError::Tds(tds_err.clone()); - let inv = InvariantError::from(manifold_err); - assert_eq!(inv, InvariantError::Tds(tds_err)); - } - - // ---- repair_stale_incident_simplices tests ---- - - #[test] - fn test_repair_stale_incident_simplices_noop_when_all_valid() { - let (mut tri, [v0, v1, v2, v3], ck) = build_single_tet(); - assert!(tri.repair_stale_incident_simplices().is_ok()); - - // Pointers unchanged. - for vk in [v0, v1, v2, v3] { - assert_eq!(tri.tds.vertex_mut(vk).unwrap().incident_simplex(), Some(ck)); - } - } - - #[test] - fn test_repair_stale_incident_simplices_repairs_none_pointer() { - let (mut tri, [_, _, _, v3], ck) = build_single_tet(); - - // Corrupt v3 to have no incident simplex. - tri.tds.vertex_mut(v3).unwrap().set_incident_simplex(None); - - assert!(tri.repair_stale_incident_simplices().is_ok()); - assert_eq!( - tri.tds.vertex_mut(v3).unwrap().incident_simplex(), - Some(ck), - "v3 should be repaired to point to the tetrahedron" - ); - } - - #[test] - fn test_repair_stale_incident_simplices_repairs_stale_pointer() { - let (mut tri, [_, _, _, v3], ck) = build_single_tet(); - - // Point v3 to a non-existent simplex key (simulates a deleted conflict simplex). - let stale = SimplexKey::from(KeyData::from_ffi(0xDEAD_BEEF)); - tri.tds - .vertex_mut(v3) - .unwrap() - .set_incident_simplex(Some(stale)); - - assert!(tri.repair_stale_incident_simplices().is_ok()); - assert_eq!( - tri.tds.vertex_mut(v3).unwrap().incident_simplex(), - Some(ck), - "stale pointer should be repaired to the valid simplex" - ); - } - - #[test] - fn test_repair_stale_incident_simplices_errors_on_truly_isolated_vertex() { - let (mut tri, _, _) = build_single_tet(); - - // Insert a vertex that is NOT referenced by any simplex. - let iso = tri - .tds - .insert_vertex_with_mapping(vertex!([0.5, 0.5, 0.5])) - .unwrap(); - - let result = tri.repair_stale_incident_simplices(); - assert!( - matches!( - &result, - Err(InsertionError::TopologyValidationFailed { - source, .. - }) if matches!( - source, - TriangulationValidationError::IsolatedVertex { vertex_key, .. } - if *vertex_key == iso - ) - ), - "Truly isolated vertex should produce IsolatedVertex error: {result:?}" - ); - } - - // ========================================================================= - // TOPOLOGY QUERY COVERAGE - // ========================================================================= - - #[test] - fn test_topology_queries_on_two_tet_triangulation() { - // 5 vertices in 3D → multi-simplex triangulation exercises all query paths - let vertices = [ - 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]), - vertex!([1.0, 1.0, 1.0]), - ]; - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - let tri = dt.as_triangulation(); - - // edges() - let edge_count = tri.number_of_edges(); - let edges_collected: HashSet<_> = tri.edges().collect(); - assert_eq!(edges_collected.len(), edge_count); - assert!(edge_count >= 6); // at least a tetrahedron's worth - - // facets() and boundary_facets() - assert!(tri.facets().next().is_some()); - assert!(tri.boundary_facets().next().is_some()); - - // simplex_vertices() and vertex_coords() - let (ck, _) = tri.simplices().next().unwrap(); - let simplex_verts = tri.simplex_vertices(ck).unwrap(); - assert_eq!(simplex_verts.len(), 4); - for &vk in simplex_verts { - let coords = tri.vertex_coords(vk).unwrap(); - assert_eq!(coords.len(), 3); - } - - // Returns None for missing keys - let missing_ck = SimplexKey::from(KeyData::from_ffi(0xDEAD)); - assert!(tri.simplex_vertices(missing_ck).is_none()); - let absent_vk = VertexKey::from(KeyData::from_ffi(0xBEEF)); - assert!(tri.vertex_coords(absent_vk).is_none()); - - // adjacent_simplices() - let v0 = tri.vertices().next().unwrap().0; - assert!(tri.adjacent_simplices(v0).next().is_some()); - - // simplex_neighbors() - // Multi-simplex triangulation has at least one internal neighbor - assert!(tri.simplex_neighbors(ck).next().is_some()); - - // incident_edges() - let inc_edges: Vec<_> = tri.incident_edges(v0).collect(); - assert!(!inc_edges.is_empty()); - assert_eq!(tri.number_of_incident_edges(v0), inc_edges.len()); - } - - // ========================================================================= - // ADJACENCY INDEX + _WITH_INDEX METHODS - // ========================================================================= - - #[test] - fn test_adjacency_index_with_index_methods() { - let vertices = [ - 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]), - vertex!([1.0, 1.0, 1.0]), - ]; - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - let tri = dt.as_triangulation(); - let index = tri.build_adjacency_index().unwrap(); - - // edges_with_index matches edges() - let idx_edges: HashSet<_> = tri.edges_with_index(&index).collect(); - let direct_edges: HashSet<_> = tri.edges().collect(); - assert_eq!(idx_edges, direct_edges); - assert_eq!( - tri.number_of_edges_with_index(&index), - tri.number_of_edges() - ); - - let v0 = tri.vertices().next().unwrap().0; - - // adjacent_simplices_with_index - let idx_adj: HashSet<_> = tri.adjacent_simplices_with_index(&index, v0).collect(); - let direct_adj: HashSet<_> = tri.adjacent_simplices(v0).collect(); - assert_eq!(idx_adj, direct_adj); - assert_eq!( - tri.number_of_adjacent_simplices_with_index(&index, v0), - direct_adj.len() - ); - - // simplex_neighbors_with_index - let ck = tri.simplices().next().unwrap().0; - let direct_neighbors: Vec<_> = tri.simplex_neighbors(ck).collect(); - assert_eq!( - tri.simplex_neighbors_with_index(&index, ck).count(), - direct_neighbors.len() - ); - assert_eq!( - tri.number_of_simplex_neighbors_with_index(&index, ck), - direct_neighbors.len() - ); - - // incident_edges_with_index - let idx_inc: HashSet<_> = tri.incident_edges_with_index(&index, v0).collect(); - let direct_inc: HashSet<_> = tri.incident_edges(v0).collect(); - assert_eq!(idx_inc, direct_inc); - assert_eq!( - tri.number_of_incident_edges_with_index(&index, v0), - direct_inc.len() - ); - } - - // ========================================================================= - // DETECT / REPAIR LOCAL FACET ISSUES - // ========================================================================= - - #[test] - fn test_detect_local_facet_issues_none_for_valid_triangulation() { - let vertices = [ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([1.0, 1.0]), - ]; - let dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - let tri = dt.as_triangulation(); - - let simplex_keys: Vec<_> = tri.simplices().map(|(ck, _)| ck).collect(); - let issues = tri.detect_local_facet_issues(&simplex_keys).unwrap(); - assert!(issues.is_none()); - } - - #[test] - fn test_ensure_non_empty_conflict_simplices_passthrough_when_nonempty() { - let mut buf = SimplexKeyBuffer::new(); - buf.push(SimplexKey::from(KeyData::from_ffi(1))); - - let owned = Cow::Owned(buf); - let fallback = SimplexKey::from(KeyData::from_ffi(999)); - let result = - Triangulation::, (), (), 2>::ensure_non_empty_conflict_simplices( - owned, fallback, - ); - assert_eq!(result.len(), 1); - } - - #[test] - fn test_ensure_non_empty_conflict_simplices_uses_fallback_when_empty() { - let buf = SimplexKeyBuffer::new(); - let fallback = SimplexKey::from(KeyData::from_ffi(42)); - let result = - Triangulation::, (), (), 2>::ensure_non_empty_conflict_simplices( - Cow::Owned(buf), - fallback, - ); - assert_eq!(result.len(), 1); - assert_eq!(result[0], fallback); - } - - #[test] - fn test_star_split_boundary_facets_produces_d_plus_1_facets() { - let ck = SimplexKey::from(KeyData::from_ffi(7)); - let facets = Triangulation::, (), (), 3>::star_split_boundary_facets(ck); - assert_eq!(facets.len(), 4); // D+1 = 4 for 3D - for (i, fh) in facets.iter().enumerate() { - assert_eq!(fh.simplex_key(), ck); - assert_eq!(>::from(fh.facet_index()), i); - } - } - // ========================================================================= - // VALIDATION REPORT - // ========================================================================= - - #[test] - fn test_validation_report_ok_for_valid_two_tet() { - let vertices = [ - 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]), - vertex!([0.5, 0.5, 0.5]), - ]; - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - assert!(dt.as_triangulation().validation_report().is_ok()); - } - - #[test] - fn test_validation_report_reports_isolated_vertex_topology_violation() { - let (mut tri, _, _) = build_single_tet(); - let _ = tri - .tds - .insert_vertex_with_mapping(vertex!([0.5, 0.5, 0.5])) - .unwrap(); - - let report = tri.validation_report().unwrap_err(); - assert!(!report.is_empty()); - assert!( - report - .violations - .iter() - .any(|v| v.kind == InvariantKind::Topology), - "Expected Topology violation in report" - ); - } - - // ========================================================================= - // TOPOLOGY GUARANTEE / VALIDATION POLICY COVERAGE - // ========================================================================= - - #[test] - fn test_topology_guarantee_requires_vertex_links_at_completion_predicate() { - assert!(TopologyGuarantee::PLManifold.requires_vertex_links_at_completion()); - assert!(TopologyGuarantee::PLManifoldStrict.requires_vertex_links_at_completion()); - assert!(!TopologyGuarantee::Pseudomanifold.requires_vertex_links_at_completion()); - } - - #[test] - fn test_validate_global_connectedness_ok_for_connected() { - let (tri, _, _) = build_single_tet(); - assert!(tri.validate_global_connectedness().is_ok()); - } - - #[test] - fn test_validate_no_isolated_vertices_ok_when_no_vertices() { - let tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - assert!(tri.validate_no_isolated_vertices().is_ok()); - } - - #[test] - fn test_pick_fan_apex_returns_none_for_empty_facets() { - let (tri, _, _) = build_single_tet(); - assert!(tri.pick_fan_apex(&[]).is_none()); - } - - // ========================================================================= - // INSERTION PIPELINE: BOOTSTRAP, INITIAL SIMPLEX, BEYOND-SIMPLEX - // ========================================================================= - - /// Helper: build a set of D+1 affinely independent vertices for dimension D. - fn simplex_vertices() -> Vec> { - let mut verts = Vec::with_capacity(D + 1); - // Origin - verts.push( - VertexBuilder::default() - .point(Point::new([0.0; D])) - .build() - .unwrap(), - ); - // Unit vectors along each axis - for i in 0..D { - let mut coords = [0.0; D]; - coords[i] = 1.0; - verts.push( - VertexBuilder::default() - .point(Point::new(coords)) - .build() - .unwrap(), - ); - } - verts - } - - /// Macro: dimension-parametric insertion pipeline tests. - macro_rules! test_insert_pipeline { - ($dim:literal) => { - pastey::paste! { - #[test] - fn []() { - let mut tri: Triangulation, (), (), $dim> = - Triangulation::new_empty(FastKernel::new()); - - // Insert fewer than D+1 vertices: should remain in bootstrap (no simplices). - let verts = simplex_vertices::<$dim>(); - for v in &verts[..$dim] { - let (vk, hint) = tri.insert(*v, None, None).unwrap(); - assert!(hint.is_none(), "{}D: no hint during bootstrap", $dim); - assert!(tri.tds.vertex(vk).is_some()); - } - assert_eq!(tri.number_of_vertices(), $dim); - assert_eq!(tri.number_of_simplices(), 0, "{}D: no simplices during bootstrap", $dim); - } - - #[test] - fn []() { - let mut tri: Triangulation, (), (), $dim> = - Triangulation::new_empty(FastKernel::new()); - - let verts = simplex_vertices::<$dim>(); - for v in &verts { - tri.insert(*v, None, None).unwrap(); - } - assert_eq!(tri.number_of_vertices(), $dim + 1); - assert_eq!(tri.number_of_simplices(), 1, "{}D: exactly 1 simplex after D+1 vertices", $dim); - - // The simplex must have D+1 vertices. - let (_, simplex) = tri.simplices().next().unwrap(); - assert_eq!(simplex.number_of_vertices(), $dim + 1); - } - - #[test] - fn []() { - let mut tri: Triangulation, (), (), $dim> = - Triangulation::new_empty(FastKernel::new()); - - // Build initial simplex. - let verts = simplex_vertices::<$dim>(); - for v in &verts { - tri.insert(*v, None, None).unwrap(); - } - assert_eq!(tri.number_of_simplices(), 1); - - // Insert an interior point. - let mut interior = [0.0; $dim]; - for c in interior.iter_mut() { - *c = 1.0 / (>::from($dim + 1) * 2.0); - } - let interior_vertex = VertexBuilder::default() - .point(Point::new(interior)) - .build() - .unwrap(); - let (_, hint) = tri - .insert(interior_vertex, None, None) - .unwrap(); - - assert!(hint.is_some(), "{}D: hint returned after D+2 insertion", $dim); - assert!(tri.number_of_simplices() > 1, "{}D: simplex count increased", $dim); - assert!(tri.is_valid().is_ok(), "{}D: topology valid after insertion", $dim); - } - } - }; - } - - test_insert_pipeline!(2); - test_insert_pipeline!(3); - test_insert_pipeline!(4); - - // ========================================================================= - // INSERT_WITH_STATISTICS: STATISTICS TRACKING - // ========================================================================= - - #[test] - fn test_insert_with_statistics_tracks_simplices_removed() { - // Build a 2D triangulation with several points, verify stats fields. - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - - let points = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [0.25, 0.25]]; - for coords in &points { - let (outcome, stats) = tri - .insert_with_statistics(vertex!(*coords), None, None) - .unwrap(); - assert!(stats.success()); - assert!(matches!(outcome, InsertionOutcome::Inserted { .. })); - assert_eq!(stats.attempts, 1); - } - assert_eq!(tri.number_of_vertices(), 4); - assert!(tri.number_of_simplices() >= 2); - } - - // ========================================================================= - // VALIDATION REPORT: MULTIPLE VIOLATIONS - // ========================================================================= - - #[test] - fn test_validation_report_collects_multiple_violations() { - // Create a triangulation with an isolated vertex AND a bad neighbor buffer - // so that validation_report collects both VertexValidity + Topology violations. - let (mut tri, _, ck) = build_single_tet(); - - // Add an isolated vertex (no incident simplex). - let _ = tri - .tds - .insert_vertex_with_mapping(vertex!([5.0, 5.0, 5.0])) - .unwrap(); - - // Corrupt a simplex's neighbor buffer length to trigger SimplexValidity violation. - let simplex = tri.tds.simplex_mut(ck).unwrap(); - simplex.ensure_neighbors_buffer_mut().truncate(2); // wrong: should be D+1 = 4 - - let report = tri.validation_report().unwrap_err(); - assert!( - report.violations.len() >= 2, - "Expected at least 2 violations, got {}", - report.violations.len() - ); - } - - // ========================================================================= - // REMOVE VERTEX: RETRIANGULATION AND TOPOLOGY - // ========================================================================= - - #[test] - fn test_remove_vertex_retriangulates_cavity_2d() { - // Build 2D triangulation with 4 vertices, remove one, verify valid. - let vertices = [ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([0.5, 0.5]), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - - let initial_simplices = dt.number_of_simplices(); - let vertex_key = dt - .vertices() - .find(|(_, v)| { - let c = v.point().coords(); - (c[0] - 0.5).abs() < 1e-10 && (c[1] - 0.5).abs() < 1e-10 - }) - .map(|(k, _)| k) - .unwrap(); - - let removed = dt.remove_vertex(vertex_key).unwrap(); - assert!(removed > 0, "Should have removed at least 1 simplex"); - assert!(dt.number_of_simplices() <= initial_simplices); - assert_eq!(dt.number_of_vertices(), 3); - } - - #[test] - fn test_remove_vertex_entire_triangulation_2d() { - // When we remove a vertex from a single-simplex triangulation, - // the empty boundary case triggers Tds::remove_vertex fallback. - let vertices = [ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - - let vertex_key = dt.vertices().next().unwrap().0; - let removed = dt.remove_vertex(vertex_key).unwrap(); - assert!(removed >= 1); - assert_eq!(dt.number_of_vertices(), 2); - } - - // ========================================================================= - // VALIDATE CONNECTEDNESS: ERROR PATHS - // ========================================================================= - - #[test] - fn test_validate_connectedness_rejects_empty_new_simplices() { - let (tri, _, _) = build_single_tet(); - - // Empty new_simplices buffer: should error because no surviving new simplices. - let empty: SimplexKeyBuffer = SimplexKeyBuffer::new(); - let err = tri.validate_connectedness(&empty).unwrap_err(); - assert!(matches!(err, InsertionError::TopologyValidation(_))); - } - - #[test] - fn test_validate_connectedness_passes_for_valid_new_simplices() { - let (tri, _, ck) = build_single_tet(); - - // Single simplex is both the new set and the whole triangulation. - let mut new_simplices = SimplexKeyBuffer::new(); - new_simplices.push(ck); - assert!(tri.validate_connectedness(&new_simplices).is_ok()); - } - - // ========================================================================= - // FIND CONFLICT REGION GLOBAL - // ========================================================================= - - #[test] - fn test_find_conflict_region_global_returns_simplices() { - // Build a 3D simplex, then check that a point outside has a conflict region. - let vertices = [ - 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 tds = - Triangulation::, (), (), 3>::build_initial_simplex(&vertices).unwrap(); - let tri = Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); - - // A point inside the circumsphere but outside the simplex. - let conflict = tri - .find_conflict_region_global(&Point::new([0.5, 0.5, 0.5])) - .unwrap(); - // The single simplex's circumsphere should contain this point. - assert!( - !conflict.is_empty(), - "Point near circumcenter should produce a conflict region" - ); - } - - /// `find_conflict_region_global` uses `sorted_simplex_points` to collect - /// vertices in canonical order. A simplex containing a missing vertex key - /// causes the helper to return `None`, which is converted to - /// `ConflictError::SimplexDataAccessFailed`. - #[test] - fn test_find_conflict_region_global_missing_vertex_returns_simplex_data_access_failed() { - let (mut tri, vkeys, ck) = build_single_tet(); - - // Replace one vertex key with a missing key. - let missing = VertexKey::from(KeyData::from_ffi(999_999)); - { - let simplex = tri.tds.simplex_mut(ck).unwrap(); - simplex.clear_vertex_keys(); - simplex.push_vertex_key(vkeys[0]); - simplex.push_vertex_key(vkeys[1]); - simplex.push_vertex_key(vkeys[2]); - simplex.push_vertex_key(missing); - } - - let result = tri.find_conflict_region_global(&Point::new([0.5, 0.5, 0.5])); - assert!( - matches!( - result, - Err(ConflictError::SimplexDataAccessFailed { simplex_key, .. }) if simplex_key == ck - ), - "expected SimplexDataAccessFailed for missing vertex, got {result:?}" - ); - } - - /// `find_conflict_region_global` checks that `sorted_simplex_points` returns - /// exactly D+1 points. A simplex with fewer than D+1 vertex keys (all - /// resolvable) triggers `SimplexDataAccessFailed` with a vertex-count message. - #[test] - fn test_find_conflict_region_global_underdimensioned_simplex_returns_simplex_data_access_failed() - { - let (mut tri, vkeys, ck) = build_single_tet(); - - // Shrink the simplex to only 3 vertices (all valid) in a D=3 triangulation. - { - let simplex = tri.tds.simplex_mut(ck).unwrap(); - simplex.clear_vertex_keys(); - simplex.push_vertex_key(vkeys[0]); - simplex.push_vertex_key(vkeys[1]); - simplex.push_vertex_key(vkeys[2]); - } - - let result = tri.find_conflict_region_global(&Point::new([0.5, 0.5, 0.5])); - assert!( - matches!( - result, - Err(ConflictError::SimplexDataAccessFailed { simplex_key, .. }) if simplex_key == ck - ), - "expected SimplexDataAccessFailed for underdimensioned simplex, got {result:?}" - ); - } - - // ========================================================================= - // CONFLICT REGION TOUCHES BOUNDARY - // ========================================================================= - - #[test] - fn test_conflict_region_touches_boundary_single_simplex() { - // A single simplex: all facets are boundary. - let vertices = [ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - - let simplex_key = tri.tds.simplex_keys().next().unwrap(); - let mut buf = SimplexKeyBuffer::new(); - buf.push(simplex_key); - - let touches = tri.conflict_region_touches_boundary(&buf).unwrap(); - assert!(touches, "Single simplex has only boundary facets"); - } - - #[test] - fn test_conflict_region_touches_boundary_empty() { - let (tri, _, _) = build_single_tet(); - let empty = SimplexKeyBuffer::new(); - let touches = tri.conflict_region_touches_boundary(&empty).unwrap(); - assert!(!touches, "Empty conflict region touches no boundary"); - } - - // ========================================================================= - // FAN FILL CAVITY: ERROR CASE - // ========================================================================= - - #[test] - fn test_fan_fill_cavity_errors_when_no_simplices_produced() { - // If the apex is on every boundary facet, fan_fill_cavity should error. - let (mut tri, vkeys, ck) = build_single_tet(); - - // Use vkeys[0] as apex; construct boundary facets that ALL include vkeys[0]. - // In a tet, facet 0 is opposite vkeys[0] (does NOT include it), - // but facets 1,2,3 each include vkeys[0]. - let boundary_facets: CavityBoundaryBuffer = - (1..=3).map(|i| FacetHandle::new(ck, i)).collect(); - - let result = tri.fan_fill_cavity(vkeys[0], &boundary_facets); - // All facets include vkeys[0], so no simplices should be created. - assert!(result.is_err()); - } - - // ========================================================================= - // REPAIR LOCAL FACET ISSUES: NON-EMPTY ISSUES MAP - // ========================================================================= - - #[test] - fn test_repair_local_facet_issues_with_overshared_facet() { - // Build 2D triangulation with enough simplices to have interior facets, - // then artificially create an over-shared facet by duplicating a simplex. - let vertices = [ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([1.0, 1.0]), - ]; - let dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - let mut tri = dt.as_triangulation().clone(); - - // Add a duplicate simplex with the same vertices as an existing simplex. - let (_, existing_simplex) = tri.tds.simplices().next().unwrap(); - let vkeys: Vec<_> = existing_simplex.vertices().to_vec(); - let dup_simplex = Simplex::new(vkeys, None).unwrap(); - let _ = tri - .tds - .insert_simplex_bypassing_topology_checks_for_test(dup_simplex) - .unwrap(); - - // Now detect issues. - let all_simplices: Vec<_> = tri.tds.simplex_keys().collect(); - let issues = tri.detect_local_facet_issues(&all_simplices).unwrap(); - assert!(issues.is_some(), "Should detect over-shared facet"); - - let removed = tri.repair_local_facet_issues(&issues.unwrap()).unwrap(); - assert!(removed > 0, "Should remove at least one duplicate simplex"); - } - - /// Return the facet index opposite the vertex not on the tested shared edge. - fn shared_edge_facet_index( - simplex: &Simplex, - v_a: VertexKey, - v_b: VertexKey, - ) -> usize { - simplex - .vertices() - .iter() - .position(|&vertex_key| vertex_key != v_a && vertex_key != v_b) - .expect("test simplices should contain the shared edge") - } - - /// Read the neighbor slot across the tested shared edge in a 2D repair fixture. - fn neighbor_across_shared_edge( - tri: &Triangulation, (), (), 2>, - simplex_key: SimplexKey, - v_a: VertexKey, - v_b: VertexKey, - ) -> Option { - let simplex = tri.tds.simplex(simplex_key).unwrap(); - let facet_idx = shared_edge_facet_index(simplex, v_a, v_b); - simplex.neighbor_key(facet_idx).flatten() - } - - #[test] - fn test_local_repair_uses_removal_frontier() { - let mut tds: Tds = Tds::empty(); - - let v_a = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); - let v_b = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); - let v_c = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); - let v_d = tds - .insert_vertex_with_mapping(vertex!([0.0, -1.0])) - .unwrap(); - let v_e = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap(); - - let c1 = tds - .insert_simplex_with_mapping(Simplex::new(vec![v_a, v_b, v_c], None).unwrap()) - .unwrap(); - let c2 = tds - .insert_simplex_with_mapping(Simplex::new(vec![v_a, v_b, v_d], None).unwrap()) - .unwrap(); - let c3 = tds - .insert_simplex_bypassing_topology_checks_for_test( - Simplex::new(vec![v_a, v_b, v_e], None).unwrap(), - ) - .unwrap(); - - for (simplex_key, neighbor_key) in [(c1, c2), (c2, c3), (c3, c1)] { - let simplex = tds.simplex_mut(simplex_key).unwrap(); - let mut neighbors = NeighborBuffer::>::new(); - neighbors.resize(3, None); - neighbors[2] = Some(neighbor_key); - simplex.set_neighbors_from_keys(neighbors).unwrap(); - } - tds.assign_incident_simplices().unwrap(); - - let mut tri = - Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); - let original_simplices = [c1, c2, c3]; - let issues = tri - .detect_local_facet_issues(&original_simplices) - .unwrap() - .expect("three simplices sharing one edge should be detected as over-shared"); - - let repair = tri - .repair_local_facet_issues_with_frontier(&issues) - .unwrap(); - assert_eq!(repair.removed_count, 1); - assert!( - !repair.frontier_simplices.is_empty(), - "removed-simplex neighbors should seed the local repair frontier" - ); - - let survivors: Vec<_> = original_simplices - .into_iter() - .filter(|simplex_key| tri.tds.contains_simplex(*simplex_key)) - .collect(); - assert_eq!(survivors.len(), 2); - let [first_survivor, second_survivor] = survivors.as_slice() else { - panic!("fixture should leave exactly two surviving simplices"); - }; - for &survivor in &survivors { - assert!( - repair.frontier_simplices.contains(&survivor), - "facet-issue survivors should seed the local repair frontier" - ); - } - let survivor_pairs = [ - (*first_survivor, *second_survivor), - (*second_survivor, *first_survivor), - ]; - - let missing_shared_slots_before = survivor_pairs - .iter() - .filter(|&&(simplex_key, other)| { - neighbor_across_shared_edge(&tri, simplex_key, v_a, v_b) != Some(other) - }) - .count(); - assert!( - missing_shared_slots_before > 0, - "simplex removal should leave at least one survivor slot needing local repair" - ); - - let mut new_simplices = SimplexKeyBuffer::new(); - new_simplices.extend(original_simplices); - let repaired = tri - .repair_neighbors_after_local_simplex_removal( - &new_simplices, - &repair.frontier_simplices, - ) - .unwrap(); - - assert!(repaired > 0); - for (simplex_key, other) in survivor_pairs { - assert_eq!( - neighbor_across_shared_edge(&tri, simplex_key, v_a, v_b), - Some(other), - "surviving simplices should be rewired across the formerly over-shared edge" - ); - } - assert!(tri.tds.validate_facet_sharing().is_ok()); - assert!(tri.detect_local_facet_issues(&survivors).unwrap().is_none()); - } - - // ========================================================================= - // DUPLICATE COORDINATES ERROR: LINEAR FALLBACK (NO INDEX) - // ========================================================================= - - #[test] - fn test_duplicate_coordinates_error_linear_scan_no_index() { - let mut tri: Triangulation, (), (), 2> = - Triangulation::new_empty(FastKernel::new()); - let _ = tri - .tds - .insert_vertex_with_mapping(vertex!([3.0, 4.0])) - .unwrap(); - - let tol = 1e-10_f64; - // No index provided: should fall back to linear scan. - let err = tri.duplicate_coordinates_error(&[3.0, 4.0], tol, None); - assert!(matches!( - err, - Some(InsertionError::DuplicateCoordinates { .. }) - )); - - // Non-duplicate should return None. - let no_err = tri.duplicate_coordinates_error(&[99.0, 99.0], tol, None); - assert!(no_err.is_none()); - } - - // ========================================================================= - // VALIDATE_AFTER_INSERTION: EDGE CASES - // ========================================================================= - - #[test] - fn test_validate_after_insertion_ok_for_valid_simplex() { - // Use a properly constructed triangulation (with neighbors/incidence). - let vertices = [ - 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::new(&vertices).unwrap(); - let tri = dt.as_triangulation(); - let suspicion = SuspicionFlags { - repair_loop_entered: true, - ..Default::default() - }; - // With Always policy and a suspicious flag, validation should still pass. - assert!( - tri.validate_after_insertion_with_scope(suspicion, None) - .is_ok() - ); - } - - // ========================================================================= - // INVARIANT ERROR CONVERSION - // ========================================================================= - - #[test] - fn test_invariant_error_to_insertion_error_all_arms() { - // Tds arm - let tds_err = InvariantError::Tds(TdsError::InvalidNeighbors { - reason: NeighborValidationError::Other { - message: "test".into(), - }, - }); - let ie = Triangulation::, (), (), 2>::invariant_error_to_insertion_error( - tds_err, - ); - assert!(matches!(ie, InsertionError::TopologyValidation(_))); - - // Triangulation arm - let tri_err = InvariantError::Triangulation( - TriangulationValidationError::EulerCharacteristicMismatch { - computed: 0, - expected: 1, - classification: TopologyClassification::Unknown, - }, - ); - let ie = Triangulation::, (), (), 2>::invariant_error_to_insertion_error( - tri_err, - ); - assert!(matches!( - ie, - InsertionError::TopologyValidationFailed { .. } - )); - - // Delaunay arm - let dt_err = - InvariantError::Delaunay(DelaunayTriangulationValidationError::VerificationFailed { - message: "test violation".to_string(), - }); - let ie = - Triangulation::, (), (), 2>::invariant_error_to_insertion_error(dt_err); - assert!(matches!( - ie, - InsertionError::DelaunayValidationFailed { .. } - )); - } - - // ========================================================================= - // ESTIMATE LOCAL PERTURBATION SCALE: NO VERTICES - // ========================================================================= - #[test] - fn test_estimate_local_perturbation_scale_no_vertices() { - let tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - let scale = tri.estimate_local_perturbation_scale(&[1.0, 2.0, 3.0], None); - // With no vertices, scale should be 1.0 (the default). - approx::assert_abs_diff_eq!(scale, 1.0, epsilon = 1e-12); - } - - // ========================================================================= - // VALIDATE_AT_COMPLETION: VARIOUS GUARANTEES - // ========================================================================= - - #[test] - fn test_validate_at_completion_ok_for_pseudomanifold_empty() { - let mut tri: Triangulation, (), (), 3> = - Triangulation::new_empty(FastKernel::new()); - tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - // Pseudomanifold does not require vertex links at completion. - assert!(tri.validate_at_completion().is_ok()); - } - - #[test] - fn test_validate_at_completion_ok_for_pl_manifold_no_simplices() { - let tri: Triangulation, (), (), 3> = + fn set_simplex_data_returns_none_for_invalid_key() { + let mut tri: Triangulation, (), i32, 2> = Triangulation::new_empty(FastKernel::new()); - // PLManifold requires vertex links at completion, but with 0 simplices it short-circuits. - assert!(tri.validate_at_completion().is_ok()); - } - - // ========================================================================= - // PROGRESSIVE PERTURBATION: SCALE INVARIANCE - // ========================================================================= - - /// Construct the same 3D geometry at three different uniform scales and verify - /// that the same number of vertices are successfully inserted at each scale. - /// This validates that perturbation is proportional to local feature size. - #[test] - fn test_perturbation_scale_invariance_3d() { - const EXPECTED_VERTEX_COUNT: usize = 8; - const EXPECTED_SIMPLEX_COUNT: usize = 10; - - fn build_at_scale(scale: f64) -> (usize, usize) { - let base_coords: [[f64; 3]; 8] = [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0], - [1.0, 1.0, 0.0], - [1.0, 0.0, 1.0], - [0.0, 1.0, 1.0], - [0.5, 0.5, 0.5], - ]; - let vertices: Vec> = base_coords - .iter() - .map(|c| vertex!([c[0] * scale, c[1] * scale, c[2] * scale])) - .collect(); - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - (dt.number_of_vertices(), dt.number_of_simplices()) - } - - let (v1, c1) = build_at_scale(1.0); - let (v2, c2) = build_at_scale(1e6); - let (v3, c3) = build_at_scale(1e-6); - - // Absolute expectations: catch regressions that affect all scales equally. - assert_eq!( - v1, EXPECTED_VERTEX_COUNT, - "Vertex count regression at unit scale (build_at_scale(1.0))" - ); - assert_eq!( - c1, EXPECTED_SIMPLEX_COUNT, - "Simplex count regression at unit scale (build_at_scale(1.0))" - ); - - // Cross-scale equality: perturbation is proportional to local feature size. - assert_eq!( - v1, v2, - "Vertex count should be scale-invariant (×1 vs ×1e6)" - ); - assert_eq!( - v1, v3, - "Vertex count should be scale-invariant (×1 vs ×1e-6)" - ); - assert_eq!( - c1, c2, - "Simplex count should be scale-invariant (×1 vs ×1e6)" - ); - assert_eq!( - c1, c3, - "Simplex count should be scale-invariant (×1 vs ×1e-6)" - ); - } - - /// Verify the mantissa-based epsilon selection (`1e-4` for f32, `1e-8` for f64) - /// and exercise the perturbation retry path with a near-degenerate simplex. - #[test] - fn test_perturbation_epsilon_selection_and_retry() { - // Assert the mantissa-digits → epsilon branching for each scalar type. - // insert_transactional uses: `if K::Scalar::mantissa_digits() <= 24 { 1e-4 } else { 1e-8 }` - assert_eq!( - f32::mantissa_digits(), - 24, - "f32 should take the 1e-4 epsilon path" - ); - assert_eq!( - f64::mantissa_digits(), - 53, - "f64 should take the 1e-8 epsilon path" - ); - - // f32 path: build a 2D triangulation, then insert a point exactly on an - // existing edge. This near-degenerate configuration exercises the full - // insert_transactional path including epsilon_value computation. - let initial_f32: Vec> = vec![ - vertex!([0.0_f32, 0.0]), - vertex!([1.0_f32, 0.0]), - vertex!([0.0_f32, 1.0]), - ]; - let tds_f32 = - Triangulation::, (), (), 2>::build_initial_simplex(&initial_f32) - .unwrap(); - let mut tri_f32 = Triangulation::, (), (), 2>::new_with_tds( - AdaptiveKernel::::new(), - tds_f32, - ); - - // Point on edge [0,0]→[1,0]: collinear, exercises degeneracy handling. - let (outcome_f32, stats_f32) = tri_f32 - .insert_with_statistics(vertex!([0.5_f32, 0.0]), None, None) - .unwrap(); - // Should succeed (SoS resolves) or be gracefully skipped. - assert!( - stats_f32.attempts >= 1, - "f32 insertion must execute at least 1 attempt" - ); - if let InsertionOutcome::Inserted { .. } = outcome_f32 { - assert_eq!(tri_f32.tds.number_of_vertices(), 4); - } - - // f64 path: same exercise with double precision. - let initial_f64: Vec> = vec![ - vertex!([0.0_f64, 0.0]), - vertex!([1.0_f64, 0.0]), - vertex!([0.0_f64, 1.0]), - ]; - let tds_f64 = - Triangulation::, (), (), 2>::build_initial_simplex(&initial_f64) - .unwrap(); - let mut tri_f64 = Triangulation::, (), (), 2>::new_with_tds( - AdaptiveKernel::::new(), - tds_f64, - ); - - let (outcome_f64, stats_f64) = tri_f64 - .insert_with_statistics(vertex!([0.5_f64, 0.0]), None, None) - .unwrap(); - assert!( - stats_f64.attempts >= 1, - "f64 insertion must execute at least 1 attempt" - ); - if let InsertionOutcome::Inserted { .. } = outcome_f64 { - assert_eq!(tri_f64.tds.number_of_vertices(), 4); - } - } - - /// Verify the `DEFAULT_PERTURBATION_RETRIES` constant value. - #[test] - fn test_default_perturbation_retries_constant() { - assert_eq!( - DEFAULT_PERTURBATION_RETRIES, 3, - "Default perturbation retries should be 3 (4 total attempts)" - ); - } - - // ========================================================================= - // PROGRESSIVE PERTURBATION: RETRY PATH COVERAGE - // ========================================================================= - - #[expect( - clippy::too_many_lines, - reason = "Literal 4D repro point set keeps retry-path coverage deterministic" - )] - fn perturbation_retry_repro_points_4d() -> [Point; 20] { - // Fixed adversarial insertion sequence captured from the former - // randomized sweep (seed 4, index 19). The final insertion exhausts - // perturbation retries in the current 4D path, so this keeps retry - // coverage deterministic without looping over random seeds. - [ - Point::new([ - 0.660_063_804_566_304_3, - 3.139_352_812_821_116, - 1.460_437_437_858_557_2, - 1.683_976_950_416_514_7, - ]), - Point::new([ - 2.451_966_162_957_145, - 9.547_229_335_697_903, - 3.306_128_696_560_687_5, - -3.722_166_730_957_705_6, - ]), - Point::new([ - -2.344_360_378_074_79, - -2.755_831_029_562_339, - -1.275_699_073_649_171_6, - 7.667_812_493_160_508, - ]), - Point::new([ - -8.633_692_230_033_44, - 1.995_093_685_275_964_6, - 7.993_316_108_703_105, - -3.310_780_098_197_376_7, - ]), - Point::new([ - 9.710_410_828_147_591, - -9.675_293_457_452_888, - -7.169_080_272_753_141, - 5.405_946_111_675_925_5, - ]), - Point::new([ - 2.266_246_031_487_613, - 2.481_673_939_102_995, - 3.039_413_140_674_462, - 4.441_464_307_622_285, - ]), - Point::new([ - 2.565_731_492_709_954, - 8.916_218_617_699_3, - -3.878_340_784_199_263_4, - -9.518_720_806_139_726, - ]), - Point::new([ - -2.067_801_258_479_087_2, - -5.739_002_626_992_522, - 7.554_154_642_458_165, - -2.983_334_995_469_171_2, - ]), - Point::new([ - 7.592_645_474_686_005, - -3.326_646_745_715_216, - -3.259_537_116_123_248, - -4.935_000_398_073_641, - ]), - Point::new([ - -5.931_807_896_262_18, - 8.897_268_005_841_394, - 0.324_049_126_782_281_15, - -8.328_532_028_712_647, - ]), - Point::new([ - -8.182_644_118_410_867, - 5.373_925_359_941_506, - -9.015_837_749_827_128, - -1.703_973_344_007_208, - ]), - Point::new([ - 1.455_467_619_488_706_2, - 9.869_985_381_801_74, - 8.605_618_759_378_327, - -1.050_236_122_559_873_3, - ]), - Point::new([ - -5.687_160_826_499_058, - 6.504_655_423_433_022, - 8.941_590_411_569_816, - 9.543_547_641_077_382, - ]), - Point::new([ - 8.975_549_245_653_312, - -8.089_655_037_805_944, - 9.936_284_142_216_682, - -7.816_992_427_475_977, - ]), - Point::new([ - 5.825_845_324_524_742, - -7.639_141_597_632_388, - 1.549_524_653_880_336_4, - 4.563_088_344_949_309, - ]), - Point::new([ - 7.387_141_055_690_918, - 6.194_972_387_680_284, - -5.764_015_058_796_046, - 9.298_338_336_238_999, - ]), - Point::new([ - -1.597_916_740_077_209_9, - -4.938_008_036_006_716, - 7.414_979_546_687_874, - -7.718_146_418_588_452, - ]), - Point::new([ - -2.414_045_007_912_424_3, - 8.888_648_260_600_007, - -5.859_329_894_512_815, - 3.268_096_825_406_147, - ]), - Point::new([ - -8.294_250_893_230_837, - 3.083_275_278_154_95, - 8.020_989_920_767_69, - 8.155_291_219_012_977, - ]), - Point::new([ - 6.718_748_825_685_814_6, - -4.640_634_945_941_695, - 2.283_644_483_657_752_7, - 0.837_537_687_473_188_8, - ]), - ] - } - - /// Exercise both successful perturbation retry (`attempt > 0`) and - /// exhaustion (`SkippedDegeneracy`) paths with deterministic 4D fixtures. - /// - /// Covers: progressive scale factor, perturbation coordinate generation - /// with `perturbation_seed == 0`, retry decision, retry success, and - /// retry exhaustion. - #[test] - fn test_perturbation_retry_and_exhaustion_4d() { - let initial_vertices: Vec> = vec![ - vertex!([0.0, 0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0, 0.0]), - vertex!([0.0, 1.0, 0.0, 0.0]), - vertex!([0.0, 0.0, 1.0, 0.0]), - vertex!([0.0, 0.0, 0.0, 1.0]), - ]; - let tds = Triangulation::, (), (), 4>::build_initial_simplex( - &initial_vertices, - ) - .unwrap(); - let mut retry_success_tri = Triangulation::, (), (), 4>::new_with_tds( - AdaptiveKernel::new(), - tds, - ); - - let _guard = ForceNextRetryableInsertionFailureGuard::enable(); - let retry_success_vertex = VertexBuilder::default() - .point(Point::new([0.2, 0.2, 0.2, 0.2])) - .build() - .unwrap(); - let (_outcome, retry_success_stats) = retry_success_tri - .insert_with_statistics(retry_success_vertex, None, None) - .unwrap(); - let saw_retry = retry_success_stats.used_perturbation() && retry_success_stats.success(); - - let mut exhaustion_tri: Triangulation, (), (), 4> = - Triangulation::new_empty(AdaptiveKernel::new()); - let mut saw_exhausted_skip = false; - - for point in perturbation_retry_repro_points_4d() { - let v = VertexBuilder::default().point(point).build().unwrap(); - let (outcome, stats) = exhaustion_tri - .insert_with_statistics(v, None, None) - .unwrap(); - - saw_exhausted_skip |= stats.skipped() - && stats.attempts == DEFAULT_PERTURBATION_RETRIES + 1 - && matches!(stats.result, InsertionResult::SkippedDegeneracy) - && matches!(outcome, InsertionOutcome::Skipped { error } if error.is_retryable()); - } - - assert!( - saw_retry, - "deterministic 4D fixture did not trigger a successful perturbation retry" - ); - assert!( - saw_exhausted_skip, - "deterministic 4D adversarial repro did not trigger retry exhaustion" - ); - } - - /// Exercise the seeded perturbation branch (`perturbation_seed != 0`) - /// by calling `insert_transactional` directly. - /// - /// Covers: the `mix` computation and sign selection in the seeded path - /// (lines using `perturbation_seed ^ ...`). - /// - /// Uses the same deterministic 4D repro as - /// [`test_perturbation_retry_and_exhaustion_4d`]. - #[test] - fn test_perturbation_retry_seeded_branch_4d() { - let mut tri: Triangulation, (), (), 4> = - Triangulation::new_empty(AdaptiveKernel::new()); - - for point in perturbation_retry_repro_points_4d() { - let v = VertexBuilder::default().point(point).build().unwrap(); - let (_outcome, stats) = tri - .insert_transactional( - v, - None, - None, - DEFAULT_PERTURBATION_RETRIES, - 0xDEAD_BEEF, - None, - None, - ) - .unwrap(); - - if stats.used_perturbation() && (stats.success() || stats.skipped()) { - return; - } - } + let stale = SimplexKey::from(KeyData::from_ffi(0xDEAD_BEEF)); - panic!("deterministic 4D adversarial repro did not trigger the seeded perturbation branch"); + assert_eq!(tri.set_simplex_data(stale, Some(42)), None); + assert_eq!(tri.tds.number_of_simplices(), 0); } } diff --git a/src/core/util/deduplication.rs b/src/core/util/deduplication.rs index 08d82458..103886d1 100644 --- a/src/core/util/deduplication.rs +++ b/src/core/util/deduplication.rs @@ -44,8 +44,8 @@ pub enum DeduplicationError { /// # Examples /// /// ``` -/// use delaunay::prelude::triangulation::dedup_vertices_exact; -/// use delaunay::prelude::triangulation::Vertex; +/// use delaunay::prelude::dedup_vertices_exact; +/// use delaunay::prelude::Vertex; /// use delaunay::prelude::geometry::Point; /// use delaunay::prelude::geometry::Coordinate; /// @@ -113,8 +113,8 @@ where /// # Examples /// /// ``` -/// use delaunay::prelude::triangulation::dedup_vertices_epsilon; -/// use delaunay::prelude::triangulation::Vertex; +/// use delaunay::prelude::dedup_vertices_epsilon; +/// use delaunay::prelude::Vertex; /// use delaunay::prelude::geometry::Point; /// use delaunay::prelude::geometry::Coordinate; /// @@ -163,7 +163,7 @@ where /// /// ```rust /// use delaunay::prelude::{DeduplicationError, try_dedup_vertices_epsilon}; -/// use delaunay::prelude::triangulation::construction::vertex; +/// use delaunay::prelude::construction::vertex; /// /// # fn main() -> Result<(), DeduplicationError> { /// let vertices = vec![ @@ -247,8 +247,8 @@ where /// # Examples /// /// ``` -/// use delaunay::prelude::triangulation::filter_vertices_excluding; -/// use delaunay::prelude::triangulation::Vertex; +/// use delaunay::prelude::filter_vertices_excluding; +/// use delaunay::prelude::Vertex; /// use delaunay::prelude::geometry::Point; /// use delaunay::prelude::geometry::Coordinate; /// diff --git a/src/core/util/delaunay_validation.rs b/src/core/util/delaunay_validation.rs index 5333c0cd..38221128 100644 --- a/src/core/util/delaunay_validation.rs +++ b/src/core/util/delaunay_validation.rs @@ -24,7 +24,7 @@ use thiserror::Error; /// /// ```rust /// use delaunay::prelude::tds::SimplexKey; -/// use delaunay::prelude::triangulation::repair::DelaunayValidationError; +/// use delaunay::prelude::repair::DelaunayValidationError; /// use slotmap::KeyData; /// /// let simplex_key = SimplexKey::from(KeyData::from_ffi(1)); @@ -84,7 +84,7 @@ pub enum DelaunayValidationError { /// /// ```rust /// use delaunay::prelude::diagnostics::delaunay_violation_report; -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0]), @@ -405,7 +405,7 @@ where /// /// ``` /// use delaunay::prelude::query::*; -/// use delaunay::prelude::triangulation::repair::find_delaunay_violations; +/// use delaunay::prelude::repair::find_delaunay_violations; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -510,7 +510,7 @@ where /// /// ```rust /// use delaunay::prelude::diagnostics::delaunay_violation_report; -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -778,7 +778,7 @@ mod tests { use crate::geometry::kernel::FastKernel; use crate::geometry::point::Point; use crate::geometry::traits::coordinate::{Coordinate, CoordinateConversionError}; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; diff --git a/src/core/util/facet_keys.rs b/src/core/util/facet_keys.rs index ae06837b..5815431a 100644 --- a/src/core/util/facet_keys.rs +++ b/src/core/util/facet_keys.rs @@ -353,7 +353,7 @@ mod tests { use crate::core::simplex::Simplex; use crate::core::util::measure_with_result; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use std::thread; diff --git a/src/core/util/facet_utils.rs b/src/core/util/facet_utils.rs index 25c698fb..603ab18a 100644 --- a/src/core/util/facet_utils.rs +++ b/src/core/util/facet_utils.rs @@ -207,7 +207,7 @@ mod tests { use super::*; use crate::core::collections::FastHashSet; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use std::time::Instant; diff --git a/src/core/util/jaccard.rs b/src/core/util/jaccard.rs index 642ac5d2..4c765a88 100644 --- a/src/core/util/jaccard.rs +++ b/src/core/util/jaccard.rs @@ -389,7 +389,7 @@ where /// /// ``` /// use delaunay::prelude::query::extract_hull_facet_set; -/// use delaunay::prelude::triangulation::DelaunayTriangulation; +/// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// @@ -604,7 +604,7 @@ mod tests { use crate::core::tds::{Tds, VertexKey}; use crate::geometry::traits::coordinate::Coordinate; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use approx::assert_relative_eq; use slotmap::KeyData; diff --git a/src/core/validation.rs b/src/core/validation.rs new file mode 100644 index 00000000..850d5c97 --- /dev/null +++ b/src/core/validation.rs @@ -0,0 +1,3020 @@ +//! Generic validation orchestration for [`Triangulation`](crate::Triangulation). +//! +//! This module owns the generic validation vocabulary and the cumulative +//! triangulation-level validation pipeline: +//! +//! - **Level 1** element validity remains implemented next to the element types: +//! [`Vertex::is_valid`](crate::core::vertex::Vertex::is_valid) and +//! [`Simplex::is_valid`](crate::core::simplex::Simplex::is_valid). +//! - **Level 2** structural validation remains implemented by +//! [`Tds`](crate::core::tds::Tds). +//! - **Level 3** topological validation is orchestrated here for +//! [`Triangulation`](crate::Triangulation). +//! +//! Delaunay-specific Level 4 validation lives in [`crate::validation`]. Keeping +//! the module boundary at the generic triangulation layer avoids one file per +//! validation level while still making the layering explicit. +//! +//! # Validation Hierarchy +//! +//! The library provides **four levels** of validation, each building on the previous: +//! +//! ## Level 1: Element Validity +//! +//! - **Methods**: [`Simplex::is_valid()`](crate::core::simplex::Simplex::is_valid), +//! [`Vertex::is_valid()`](crate::core::vertex::Vertex::is_valid) +//! - **Checks**: Basic data integrity (coordinate validity, UUID presence, proper initialization) +//! - **Cost**: O(1) per element +//! +//! ## Level 2: TDS Structural Validity +//! +//! - **Method**: [`Tds::is_valid()`](crate::core::tds::Tds::is_valid) +//! - **Checks**: +//! - UUID ↔ Key mapping consistency +//! - No duplicate simplices (same vertex sets) +//! - Facet sharing invariant (≤2 simplices per facet) +//! - Neighbor consistency (mutual relationships) +//! - **Cost**: O(N×D²) where N = simplices, D = dimension +//! +//! Use [`Tds::validate()`](crate::core::tds::Tds::validate) for cumulative +//! Levels 1–2 (element + structural) validation. +//! +//! ## Level 3: Manifold Topology +//! +//! - **Method**: [`Triangulation::is_valid()`](crate::core::triangulation::Triangulation::is_valid) +//! - **Checks**: +//! - **Codimension-1 manifoldness**: exactly 1 boundary simplex or 2 interior simplices per facet +//! - **Codimension-2 boundary manifoldness**: the boundary is closed ("no boundary of boundary") +//! - Connectedness (single connected component in the simplex neighbor graph) +//! - No isolated vertices (every vertex must be incident to at least one simplex) +//! - Euler characteristic (χ = V - E + F - C matches expected topology) +//! - **Cost**: O(N×D²) dominated by simplex counting +//! +//! Use [`Triangulation::validate()`](crate::core::triangulation::Triangulation::validate) +//! for cumulative Levels 1–3. +//! +//! ## Level 4: Delaunay Property +//! +//! - **Method**: [`DelaunayTriangulation::is_valid()`](crate::DelaunayTriangulation::is_valid) +//! - **Checks**: Empty circumsphere property (no vertex inside any simplex's circumsphere) +//! - **Cost**: O(N×V) where N = simplices, V = vertices +//! +//! Use [`DelaunayTriangulation::validate()`](crate::DelaunayTriangulation::validate) +//! for cumulative Levels 1–4. +//! +//! ## Topology guarantees +//! +//! [`TopologyGuarantee`](crate::core::validation::TopologyGuarantee) selects +//! which **manifoldness** invariants are checked by Level 3 topology validation. +//! Whether those checks run automatically after insertion is controlled by +//! [`ValidationPolicy`](crate::core::validation::ValidationPolicy). +//! +//! Level 3 validation always checks: +//! - Codimension-1 facet degree (pseudomanifold condition: 1 boundary or 2 interior simplices per facet) +//! - Codimension-2 boundary manifoldness (closed boundary: "no boundary of boundary") +//! - Connectedness (single connected component in the simplex neighbor graph) +//! - No isolated vertices (every vertex must be incident to at least one simplex) +//! - Euler characteristic +//! +//! With +//! [`TopologyGuarantee::PLManifold`](crate::core::validation::TopologyGuarantee::PLManifold), +//! Level 3 validation additionally checks the canonical **vertex-link** PL-manifoldness condition via +//! [`crate::topology::manifold::validate_vertex_links`]. +//! +//! Note: for **D=3**, the current vertex-link validator additionally enforces that each link +//! has the Euler characteristic / boundary component counts of a sphere/ball (S²/B²). +//! For **D≥4**, it currently checks that each vertex link is a connected (D−1)-manifold +//! with the correct boundary behavior (a necessary condition), but does not attempt to +//! distinguish spheres/balls from other manifolds (not sufficient in general). + +use crate::core::algorithms::incremental_insertion::InsertionError; +use crate::core::collections::{ + FacetToSimplicesMap, FastHashSet, SimplexKeyBuffer, SimplexKeySet, fast_hash_set_with_capacity, +}; +use crate::core::operations::{InsertionTelemetry, InsertionTelemetryMode, SuspicionFlags}; +use crate::core::tds::{ + InvariantError, InvariantKind, InvariantViolation, SimplexKey, TdsError, + TriangulationValidationReport, VertexKey, +}; +use crate::core::traits::data_type::DataType; +use crate::core::triangulation::Triangulation; +use crate::geometry::kernel::Kernel; +use crate::topology::characteristics::euler::{TopologyClassification, expected_chi_for}; +use crate::topology::characteristics::validation::validate_triangulation_euler_with_facet_to_simplices_map; +use crate::topology::manifold::{ + ManifoldError, validate_closed_boundary, validate_facet_degree, + validate_local_pseudomanifold_for_simplices, validate_ridge_links, + validate_ridge_links_for_simplices, validate_vertex_links, +}; +use crate::topology::traits::topological_space::{GlobalTopology, TopologyKind}; +use std::time::Instant; +use thiserror::Error; +use uuid::Uuid; + +/// Convert an [`InsertionError`] into the appropriate [`InvariantError`], preserving +/// structured error information across all layers. +/// +/// - `TopologyValidation(source)` → `InvariantError::Tds(source)` (Level 1–2 preserved) +/// - `TopologyValidationFailed { source }` → `InvariantError::Triangulation(source)` (Level 3 preserved) +/// - All other variants → `InvariantError::Tds(InconsistentDataStructure { .. })` with `context` +pub(crate) fn insertion_error_to_invariant_error( + error: InsertionError, + context: &str, +) -> InvariantError { + match error { + InsertionError::TopologyValidation(source) => InvariantError::Tds(source), + InsertionError::TopologyValidationFailed { source, .. } => { + InvariantError::Triangulation(source) + } + other => InvariantError::Tds(TdsError::InconsistentDataStructure { + message: format!("{context}: {other}"), + }), + } +} + +/// Errors that can occur during triangulation topology validation (Level 3). +/// +/// This type represents **only** Level 3 (topology) errors. It does not contain +/// TDS-level (Levels 1–2) errors. Cumulative validators that can return errors +/// from any level use [`InvariantError`] instead. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::tds::InvariantError; +/// use delaunay::prelude::*; +/// +/// 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::new(&vertices).unwrap(); +/// +/// let result: Result<(), InvariantError> = dt.as_triangulation().validate(); +/// assert!(result.is_ok()); +/// ``` +#[derive(Clone, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum TriangulationValidationError { + /// A facet belongs to an unexpected number of simplices for a manifold-with-boundary. + #[error( + "Non-manifold facet: facet {facet_key:016x} belongs to {simplex_count} simplices (expected 1 or 2)" + )] + ManifoldFacetMultiplicity { + /// The facet key with invalid multiplicity. + facet_key: u64, + /// The number of incident simplices observed. + simplex_count: usize, + }, + + /// Boundary is not a closed (D-1)-manifold: + /// wrong number of boundary facets. + /// + /// This detects "boundary of boundary" issues (codimension-2 manifoldness of the boundary). + #[error( + "Boundary is not closed: boundary ridge {ridge_key:016x} is incident to {boundary_facet_count} boundary facets (expected 2)" + )] + BoundaryRidgeMultiplicity { + /// Canonical key for the (D-2)-simplex (ridge) on the boundary. + ridge_key: u64, + /// Number of incident boundary facets observed. + boundary_facet_count: usize, + }, + + /// A ridge's link graph is not a 1-manifold (path or cycle). + /// + /// This is required for PL-manifold validation. + #[error( + "Ridge link is not a 1-manifold: ridge {ridge_key:016x} has link graph with {link_vertex_count} vertices, {link_edge_count} edges, max degree {max_degree}, degree-1 vertices {degree_one_vertices}, connected={connected} (expected connected cycle or path)" + )] + RidgeLinkNotManifold { + /// Canonical key for the (D-2)-simplex (ridge). + ridge_key: u64, + /// Number of vertices in the ridge's link graph. + link_vertex_count: usize, + /// Number of edges in the ridge's link graph. + link_edge_count: usize, + /// Maximum vertex degree observed in the link graph. + max_degree: usize, + /// Number of vertices of degree 1 observed in the link graph. + degree_one_vertices: usize, + /// Whether the link graph is connected. + connected: bool, + }, + + /// A vertex link is not a (D-1)-manifold (sphere/ball) as required for PL-manifoldness. + #[error( + "Vertex link is not a PL (D-1)-manifold: vertex {vertex_key:?} has link with {link_vertex_count} vertices, {link_simplex_count} simplices, boundary_facets={boundary_facet_count}, max_degree={max_degree}, connected={connected}, interior_vertex={interior_vertex}" + )] + VertexLinkNotManifold { + /// The vertex whose link failed validation. + vertex_key: VertexKey, + /// Number of vertices in the link (0-simplices of the link). + link_vertex_count: usize, + /// Number of (D-1)-simplices (simplices) in the link. + link_simplex_count: usize, + /// Number of boundary facets in the link (facets of degree 1). + boundary_facet_count: usize, + /// Maximum degree in the link 1-skeleton. + max_degree: usize, + /// Whether the link 1-skeleton is connected. + connected: bool, + /// Whether the vertex was classified as an interior vertex of the original complex. + interior_vertex: bool, + }, + + /// Euler characteristic does not match the expected value for the classified topology. + #[error( + "Euler characteristic mismatch: computed χ={computed}, expected χ={expected} for {classification:?}" + )] + EulerCharacteristicMismatch { + /// Computed Euler characteristic. + computed: isize, + /// Expected Euler characteristic for the classification. + expected: isize, + /// The topology classification used to determine expectation. + classification: TopologyClassification, + }, + + /// Vertex is not incident to any simplex. + /// + /// An isolated vertex violates manifold invariants at the topology (Level 3) layer + /// and may indicate a failed insertion or an insertion that was partially rolled back. + #[error( + "Isolated vertex: vertex {vertex_uuid} (key {vertex_key:?}) is not incident to any simplex" + )] + IsolatedVertex { + /// Key of the isolated vertex. + vertex_key: VertexKey, + /// UUID of the isolated vertex. + vertex_uuid: Uuid, + }, + + /// The simplex neighbor graph is not a single connected component. + /// + /// A valid triangulation-with-boundary must be connected; multiple disconnected + /// components indicate a structural problem (e.g. simplices that share only a vertex + /// or edge but no facet, so no neighbor pointers link them). + #[error( + "Disconnected triangulation: simplex neighbor graph is not a single connected component ({simplex_count} simplices total)" + )] + Disconnected { + /// Total number of simplices in the triangulation. + simplex_count: usize, + }, +} + +impl TryFrom for TriangulationValidationError { + type Error = TdsError; + + fn try_from(err: ManifoldError) -> Result { + match err { + ManifoldError::Tds(source) => Err(source), + ManifoldError::ManifoldFacetMultiplicity { + facet_key, + simplex_count, + } => Ok(Self::ManifoldFacetMultiplicity { + facet_key, + simplex_count, + }), + ManifoldError::BoundaryRidgeMultiplicity { + ridge_key, + boundary_facet_count, + } => Ok(Self::BoundaryRidgeMultiplicity { + ridge_key, + boundary_facet_count, + }), + ManifoldError::RidgeLinkNotManifold { + ridge_key, + link_vertex_count, + link_edge_count, + max_degree, + degree_one_vertices, + connected, + } => Ok(Self::RidgeLinkNotManifold { + ridge_key, + link_vertex_count, + link_edge_count, + max_degree, + degree_one_vertices, + connected, + }), + ManifoldError::VertexLinkNotManifold { + vertex_key, + link_vertex_count, + link_simplex_count, + boundary_facet_count, + max_degree, + connected, + interior_vertex, + } => Ok(Self::VertexLinkNotManifold { + vertex_key, + link_vertex_count, + link_simplex_count, + boundary_facet_count, + max_degree, + connected, + interior_vertex, + }), + } + } +} + +impl From for InvariantError { + fn from(err: ManifoldError) -> Self { + match TriangulationValidationError::try_from(err) { + Ok(source) => Self::Triangulation(source), + Err(source) => Self::Tds(source), + } + } +} + +/// Policy controlling when the triangulation runs global validation passes. +/// +/// Validation can be expensive (O(N×D²) or worse), so this allows callers to trade +/// performance for stricter correctness checks during incremental operations. +/// +/// **Note**: [`TopologyGuarantee::PLManifold`] is incompatible with [`ValidationPolicy::Never`]. +/// `PLManifold` requires at least end-of-construction validation to certify full +/// PL-manifoldness. Use [`ValidationPolicy::OnSuspicion`] (default) for best performance, +/// or [`ValidationPolicy::Always`] for maximum safety during incremental operations. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::operations::SuspicionFlags; +/// use delaunay::prelude::ValidationPolicy; +/// +/// let policy = ValidationPolicy::OnSuspicion; +/// let suspicion = SuspicionFlags { perturbation_used: true, ..SuspicionFlags::default() }; +/// assert!(policy.should_validate(suspicion)); +/// ``` +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ValidationPolicy { + /// Never run global validation. + Never, + + /// Validate only if the operation is suspicious (e.g. degeneracy). + OnSuspicion, + + /// Always validate after insertion. + Always, + + /// Debug builds: always validate; release builds: [`ValidationPolicy::OnSuspicion`]. + DebugOnly, +} + +impl ValidationPolicy { + /// Returns `true` if a global validation pass should be run given the observed + /// [`crate::core::operations::SuspicionFlags`]. + #[inline] + #[must_use] + pub const fn should_validate(&self, suspicion: SuspicionFlags) -> bool { + match self { + Self::Never => false, + Self::Always => true, + Self::OnSuspicion => suspicion.is_suspicious(), + Self::DebugOnly => cfg!(debug_assertions) || suspicion.is_suspicious(), + } + } +} + +impl Default for ValidationPolicy { + #[inline] + fn default() -> Self { + Self::OnSuspicion + } +} + +/// Selects which topological invariants are checked by Level 3 validation. +/// +/// This enum specifies *what is checked* about the underlying simplicial complex when +/// Level 3 validation runs. Whether Level 3 validation runs automatically after insertion +/// is controlled by [`ValidationPolicy`]. +/// +/// - [`TopologyGuarantee::Pseudomanifold`] checks the codimension-1 adjacency condition: +/// each facet is incident to one or two simplices, and the codimension-2 boundary is closed. +/// This is sufficient for many geometric algorithms but does not guarantee local Euclidean structure. +/// +/// - [`TopologyGuarantee::PLManifold`] uses ridge-link validation during insertion and +/// requires a vertex-link validation pass at construction completion to certify +/// PL-manifoldness. +/// - [`TopologyGuarantee::PLManifoldStrict`] runs vertex-link validation after every +/// insertion for maximal safety (slowest). +/// +/// # Example +/// +/// ```rust +/// use delaunay::prelude::*; +/// +/// 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 mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); +/// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); +/// +/// // Optional: relax topology checks for speed (weaker guarantees). +/// dt.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); +/// assert!(!dt.topology_guarantee().requires_vertex_links_at_completion()); +/// ``` +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TopologyGuarantee { + /// Validate only the pseudomanifold / manifold-with-boundary invariants: + /// - facet degree (1 or 2 incident simplices per facet) + /// - closed boundary ("no boundary of boundary") + Pseudomanifold, + + /// Validate PL-manifold invariants (incremental mode). + /// + /// This includes all `Pseudomanifold` checks plus ridge-link validation during + /// insertion, with a required vertex-link validation at construction completion. + PLManifold, + + /// Validate PL-manifold invariants with strict per-insertion checks. + /// + /// This includes all `Pseudomanifold` checks plus vertex-link validation + /// after every insertion (slowest, maximum safety). + PLManifoldStrict, +} + +impl Default for TopologyGuarantee { + #[inline] + fn default() -> Self { + Self::DEFAULT + } +} + +impl TopologyGuarantee { + /// The default topology guarantee used when constructing triangulations. + /// + /// This is a `const` alternative to `::default()` for `const fn` constructors. + pub const DEFAULT: Self = Self::PLManifold; + + /// Returns `true` if this topology guarantee requires vertex-link validation + /// after each insertion. + #[inline] + #[must_use] + pub const fn requires_vertex_links_during_insertion(self) -> bool { + matches!(self, Self::PLManifoldStrict) + } + + /// Returns `true` if this topology guarantee requires vertex-link validation + /// at construction completion. + #[inline] + #[must_use] + pub const fn requires_vertex_links_at_completion(self) -> bool { + matches!(self, Self::PLManifold | Self::PLManifoldStrict) + } + + /// Returns `true` if this topology guarantee requires pseudomanifold checks + /// during insertion. + /// + /// All current guarantees require the codimension-1 facet-degree and + /// codimension-2 closed-boundary conditions. Stronger guarantees layer + /// ridge-link and vertex-link validation on top of these checks. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::TopologyGuarantee; + /// + /// assert!( + /// TopologyGuarantee::Pseudomanifold + /// .requires_pseudomanifold_checks_during_insertion() + /// ); + /// assert!( + /// TopologyGuarantee::PLManifold + /// .requires_pseudomanifold_checks_during_insertion() + /// ); + /// ``` + #[inline] + #[must_use] + pub const fn requires_pseudomanifold_checks_during_insertion(self) -> bool { + matches!( + self, + Self::Pseudomanifold | Self::PLManifold | Self::PLManifoldStrict + ) + } + + /// Returns `true` if this topology guarantee requires ridge-link validation. + /// + /// Ridge-link validation is fast (O(local)) and catches many PL-manifold violations, + /// providing good error detection even with reduced validation frequency. + #[inline] + #[must_use] + pub const fn requires_ridge_links(self) -> bool { + matches!(self, Self::PLManifold | Self::PLManifoldStrict) + } + + /// Returns the [`ValidationPolicy`] that should be used by default for this guarantee. + /// + /// [`PLManifoldStrict`](Self::PLManifoldStrict) uses [`Always`](ValidationPolicy::Always) + /// so that full Level-3 global validation (including vertex-link checks) runs + /// after every insertion — this is the strongest and slowest setting. + /// All other guarantees default to + /// [`OnSuspicion`](ValidationPolicy::OnSuspicion). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::{TopologyGuarantee, ValidationPolicy}; + /// + /// assert_eq!( + /// TopologyGuarantee::PLManifoldStrict.default_validation_policy(), + /// ValidationPolicy::Always, + /// ); + /// assert_eq!( + /// TopologyGuarantee::PLManifold.default_validation_policy(), + /// ValidationPolicy::OnSuspicion, + /// ); + /// ``` + #[inline] + #[must_use] + pub const fn default_validation_policy(self) -> ValidationPolicy { + match self { + Self::PLManifoldStrict => ValidationPolicy::Always, + _ => ValidationPolicy::OnSuspicion, + } + } + + /// Returns `true` if this guarantee is compatible with the given validation policy. + /// + /// `PLManifold` requires at least end-of-construction validation, so it's incompatible + /// with `ValidationPolicy::Never`. + #[inline] + #[must_use] + pub const fn is_compatible_with_policy(self, policy: ValidationPolicy) -> bool { + match self { + Self::Pseudomanifold => true, + Self::PLManifold | Self::PLManifoldStrict => !matches!(policy, ValidationPolicy::Never), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum InsertionValidationWork { + FullValidation, + RequiredTopologyLinks, +} + +impl Triangulation +where + K: Kernel, +{ + /// Returns the topology guarantee used for Level 3 topology validation. + #[inline] + #[must_use] + pub const fn topology_guarantee(&self) -> TopologyGuarantee { + self.topology_guarantee + } + + /// Returns the runtime global topology metadata associated with this triangulation. + #[inline] + #[must_use] + pub const fn global_topology(&self) -> GlobalTopology { + self.global_topology + } + + /// Returns the high-level topology kind (`Euclidean`, `Toroidal`, etc.). + #[inline] + #[must_use] + pub const fn topology_kind(&self) -> TopologyKind { + self.global_topology.kind() + } + + /// Sets runtime global topology metadata on the triangulation. + #[inline] + pub const fn set_global_topology(&mut self, global_topology: GlobalTopology) { + self.global_topology = global_topology; + } + + /// Returns the insertion-time global topology validation policy used by the triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::{Triangulation, ValidationPolicy}; + /// use delaunay::prelude::geometry::FastKernel; + /// + /// let tri: Triangulation, (), (), 2> = + /// Triangulation::new_empty(FastKernel::new()); + /// + /// assert_eq!(tri.validation_policy(), ValidationPolicy::OnSuspicion); + /// ``` + #[inline] + #[must_use] + pub const fn validation_policy(&self) -> ValidationPolicy { + self.validation_policy + } + + /// Sets the insertion-time global topology validation policy used by the triangulation. + /// + /// If the requested policy is incompatible with the current topology guarantee (for example, + /// `ValidationPolicy::Never` with `TopologyGuarantee::PLManifold`), this runs + /// [`Triangulation::validate_at_completion`](Self::validate_at_completion) to provide + /// immediate feedback and emits a warning. Call `validate_at_completion()` after batch + /// construction when using an incompatible combination. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::{Triangulation, ValidationPolicy}; + /// use delaunay::prelude::geometry::FastKernel; + /// + /// let mut tri: Triangulation, (), (), 2> = + /// Triangulation::new_empty(FastKernel::new()); + /// + /// tri.set_validation_policy(ValidationPolicy::Always); + /// assert_eq!(tri.validation_policy(), ValidationPolicy::Always); + /// ``` + #[inline] + pub fn set_validation_policy(&mut self, policy: ValidationPolicy) { + if !self.topology_guarantee.is_compatible_with_policy(policy) { + let completion_result = self.validate_at_completion(); + + if let Err(err) = completion_result { + debug_assert!( + false, + "Validation policy {policy:?} is incompatible with topology guarantee {guarantee:?}; validate_at_completion failed: {err}", + guarantee = self.topology_guarantee + ); + tracing::warn!( + "Validation policy {policy:?} is incompatible with topology guarantee {guarantee:?}; validate_at_completion failed: {err}. Validation policy not updated.", + guarantee = self.topology_guarantee + ); + return; + } + + tracing::warn!( + "Validation policy {policy:?} is incompatible with topology guarantee {guarantee:?}; call validate_at_completion() after construction to certify PL-manifoldness.", + guarantee = self.topology_guarantee + ); + } + + self.validation_policy = policy; + } + + /// Sets the topology guarantee used for Level 3 topology validation. + /// + /// If the requested guarantee is incompatible with the current validation policy (for + /// example, `ValidationPolicy::Never` with `TopologyGuarantee::PLManifold`), this runs + /// [`Triangulation::validate_at_completion`](Self::validate_at_completion) to provide + /// immediate feedback and emits a warning. Call `validate_at_completion()` after batch + /// construction when using an incompatible combination. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::{TopologyGuarantee, Triangulation}; + /// use delaunay::prelude::geometry::FastKernel; + /// + /// let mut tri: Triangulation, (), (), 2> = + /// Triangulation::new_empty(FastKernel::new()); + /// tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + /// assert_eq!(tri.topology_guarantee(), TopologyGuarantee::Pseudomanifold); + /// ``` + #[inline] + pub fn set_topology_guarantee(&mut self, guarantee: TopologyGuarantee) { + if !guarantee.is_compatible_with_policy(self.validation_policy) { + let previous = self.topology_guarantee; + self.topology_guarantee = guarantee; + let completion_result = self.validate_at_completion(); + + if let Err(err) = completion_result { + self.topology_guarantee = previous; + debug_assert!( + false, + "Topology guarantee {guarantee:?} is incompatible with validation policy {policy:?}; validate_at_completion failed: {err}", + policy = self.validation_policy + ); + tracing::warn!( + "Topology guarantee {guarantee:?} is incompatible with validation policy {policy:?}; validate_at_completion failed: {err}. Topology guarantee not updated.", + policy = self.validation_policy + ); + return; + } + + self.topology_guarantee = previous; + tracing::warn!( + "Topology guarantee {guarantee:?} is incompatible with validation policy {policy:?}; call validate_at_completion() after construction to certify PL-manifoldness.", + policy = self.validation_policy + ); + } + + self.topology_guarantee = guarantee; + } + + /// Traverses the simplex neighbor graph for validation without assuming global connectivity. + /// + /// If `allowed` is `Some`, traversal is restricted to that set. Neighbors + /// outside the allowed set are reported through `on_external_neighbor` so + /// localized validation can still prove the new component attaches to the + /// existing triangulation. + #[must_use] + fn traverse_simplex_neighbor_graph( + &self, + start: SimplexKey, + reserve: usize, + allowed: Option<&SimplexKeySet>, + mut on_external_neighbor: F, + ) -> SimplexKeySet + where + F: FnMut(SimplexKey, SimplexKey), + { + let mut visited: SimplexKeySet = SimplexKeySet::default(); + visited.reserve(reserve); + + let mut stack: SimplexKeyBuffer = SimplexKeyBuffer::new(); + stack.push(start); + + while let Some(ck) = stack.pop() { + if !visited.insert(ck) { + continue; + } + + let Some(simplex) = self.tds.simplex(ck) else { + continue; + }; + + let Some(neighbors) = simplex.neighbor_keys() else { + continue; + }; + + for n_opt in neighbors { + let Some(nk) = n_opt else { + continue; + }; + + if !self.tds.contains_simplex(nk) { + continue; + } + + if allowed.is_some_and(|allowed| !allowed.contains(&nk)) { + on_external_neighbor(ck, nk); + continue; + } + + if !visited.contains(&nk) { + stack.push(nk); + } + } + } + + visited + } + + /// Validates topological invariants of the triangulation (Level 3). + /// + /// This checks the triangulation/topology layer **only**: + /// - Codimension-1 pseudomanifold condition: each facet is incident to 1 (boundary) or 2 (interior) simplices + /// - Codimension-2 boundary manifoldness: the boundary must be closed ("no boundary of boundary") + /// - Geometric orientation-sign consistency for stored simplices (signed determinant > 0) + /// - Ridge-link validation (when `topology_guarantee.requires_ridge_links()`) + /// - Vertex-link validation during insertion (when `topology_guarantee.requires_vertex_links_during_insertion()`) + /// - Connectedness (single component in the simplex neighbor graph) + /// - No isolated vertices (every vertex must be incident to at least one simplex) + /// - Euler characteristic + /// + /// For `TopologyGuarantee::PLManifold`, full PL-manifold certification requires + /// calling [`Triangulation::validate_at_completion`](Self::validate_at_completion) + /// (or [`Triangulation::validate`](Self::validate)) after batch construction. + /// + /// It intentionally does **not** validate lower layers (vertices/simplices or TDS structure). + /// For cumulative validation, use [`Triangulation::validate`](Self::validate). + /// + /// # Errors + /// + /// Returns an [`InvariantError`] if: + /// - The manifold-with-boundary facet property is violated. + /// - The triangulation is disconnected (multiple simplex components). + /// - An isolated vertex is detected (no incident simplex). + /// - Euler characteristic validation fails. + /// - The topology module reports an error (treated as inconsistent data structure). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// let vertices_4d = [ + /// vertex!([0.0, 0.0, 0.0, 0.0]), + /// vertex!([1.0, 0.0, 0.0, 0.0]), + /// vertex!([0.0, 1.0, 0.0, 0.0]), + /// vertex!([0.0, 0.0, 1.0, 0.0]), + /// vertex!([0.0, 0.0, 0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 4> = + /// DelaunayTriangulation::new(&vertices_4d).unwrap(); + /// + /// // Level 3: topology validation (manifold-with-boundary + Euler characteristic) + /// assert!(dt.as_triangulation().is_valid().is_ok()); + /// ``` + pub fn is_valid(&self) -> Result<(), InvariantError> { + self.validate_topology_core()?; + // Check geometric orientation after manifold/link checks so topology-specific + // diagnostics surface first when multiple invariants are violated. + self.validate_geometric_simplex_orientation()?; + Ok(()) + } + + /// Validates topological invariants **without** geometric orientation checks. + /// + /// This is identical to [`is_valid`](Self::is_valid) but omits the + /// `validate_geometric_simplex_orientation()` step. It is intended for + /// explicit combinatorial construction where the user-provided vertex + /// orderings may produce negative determinants that are nonetheless + /// topologically valid. + pub(crate) fn is_valid_topology_only(&self) -> Result<(), InvariantError> { + self.validate_topology_core() + } + + /// Shared Level-3 topology validation sequence used by both [`is_valid`](Self::is_valid) + /// and [`is_valid_topology_only`](Self::is_valid_topology_only). + /// + /// Checks connectedness, manifold facet degree, closed boundary, ridge/vertex + /// links (when required by the topology guarantee), isolated vertices, and + /// Euler characteristic. + fn validate_topology_core(&self) -> Result<(), InvariantError> { + // 1. Connectedness + // + // Checked first because it is cheaper than building the facet-to-simplices map + // (which requires O(N·D) hash-map insertions plus allocations) and avoids + // all subsequent work when the triangulation is disconnected. + self.validate_global_connectedness()?; + + // 2. Manifold facet multiplicity (codimension-1 pseudomanifold condition) + // + // Build the facet map once and reuse it for manifold validation and Euler counting. + let facet_to_simplices: FacetToSimplicesMap = self.tds.build_facet_to_simplices_map()?; + self.validate_topology_core_with_facet_to_simplices_map(&facet_to_simplices) + } + + fn validate_topology_core_with_facet_to_simplices_map( + &self, + facet_to_simplices: &FacetToSimplicesMap, + ) -> Result<(), InvariantError> { + validate_facet_degree(facet_to_simplices)?; + + // 2b. Boundary manifoldness in codimension 2: the boundary must be "closed" + // (i.e., its ridges must have degree 2 within boundary facets). + validate_closed_boundary(&self.tds, facet_to_simplices)?; + + // 2c. Ridge-link validation for PLManifold/PLManifoldStrict (fast, catches many PL issues). + if self.topology_guarantee.requires_ridge_links() { + validate_ridge_links(&self.tds)?; + } + // 2d. PL-manifold vertex-link condition during insertion (strict mode). + if self + .topology_guarantee + .requires_vertex_links_during_insertion() + { + validate_vertex_links(&self.tds, facet_to_simplices)?; + } + + // 3. Vertex incidence (manifold invariant): every vertex must be incident to at least one simplex. + self.validate_no_isolated_vertices()?; + + // 4. Euler characteristic using the topology module + let topology_result = + validate_triangulation_euler_with_facet_to_simplices_map(&self.tds, facet_to_simplices); + + // Override the heuristic classification when the caller has declared a + // non-Euclidean global topology. The heuristic classifies any closed + // mesh (no boundary facets) as `ClosedSphere(D)`, but a toroidal mesh + // also has no boundary — its expected χ is 0, not 1+(-1)^D. + let (classification, expected) = match self.global_topology { + GlobalTopology::Toroidal { .. } + if matches!( + topology_result.classification, + TopologyClassification::ClosedSphere(_) + ) => + { + let cls = TopologyClassification::ClosedToroid(D); + (cls, expected_chi_for(&cls)) + } + _ => (topology_result.classification, topology_result.expected), + }; + + if let Some(exp) = expected + && topology_result.chi != exp + { + return Err(TriangulationValidationError::EulerCharacteristicMismatch { + computed: topology_result.chi, + expected: exp, + classification, + } + .into()); + } + + Ok(()) + } + + /// Validates vertex-link condition at construction completion. + /// + /// This should be called once after batch construction is complete to certify + /// full PL-manifoldness when using `TopologyGuarantee::PLManifold` (incremental mode). + /// + /// # Errors + /// + /// Returns an [`InvariantError`] if vertex-link validation fails + /// (e.g. a vertex link is not a PL-sphere/ball as required for PL-manifoldness). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// 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::new(&vertices).unwrap(); + /// assert!(dt.as_triangulation().validate_at_completion().is_ok()); + /// ``` + pub fn validate_at_completion(&self) -> Result<(), InvariantError> { + if !self + .topology_guarantee + .requires_vertex_links_at_completion() + { + return Ok(()); + } + + if self.tds.number_of_simplices() == 0 { + return Ok(()); + } + + let facet_to_simplices: FacetToSimplicesMap = self.tds.build_facet_to_simplices_map()?; + self.validate_at_completion_with_facet_to_simplices_map(&facet_to_simplices)?; + Ok(()) + } + + fn validate_at_completion_with_facet_to_simplices_map( + &self, + facet_to_simplices: &FacetToSimplicesMap, + ) -> Result<(), InvariantError> { + if !self + .topology_guarantee + .requires_vertex_links_at_completion() + { + return Ok(()); + } + + if self.tds.number_of_simplices() == 0 { + return Ok(()); + } + + validate_vertex_links(&self.tds, facet_to_simplices)?; + Ok(()) + } + + /// Performs cumulative validation for Levels 1–3. + /// + /// This validates: + /// - **Level 1–2** via [`Tds::validate`](crate::core::tds::Tds::validate) + /// - **Level 3** via [`Triangulation::is_valid`](Self::is_valid) + /// - **Completion-time PL-manifold check** via [`Triangulation::validate_at_completion`](Self::validate_at_completion) + /// + /// # Errors + /// + /// Returns an [`InvariantError`] if: + /// - Any vertex/simplex is invalid (Level 1). + /// - The TDS structural invariants fail (Level 2). + /// - Topology validation fails (Level 3). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// let vertices_4d = [ + /// vertex!([0.0, 0.0, 0.0, 0.0]), + /// vertex!([1.0, 0.0, 0.0, 0.0]), + /// vertex!([0.0, 1.0, 0.0, 0.0]), + /// vertex!([0.0, 0.0, 1.0, 0.0]), + /// vertex!([0.0, 0.0, 0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 4> = + /// DelaunayTriangulation::new(&vertices_4d).unwrap(); + /// + /// // Levels 1–3: elements + TDS structure + topology + /// assert!(dt.as_triangulation().validate().is_ok()); + /// ``` + pub fn validate(&self) -> Result<(), InvariantError> + where + U: DataType, + V: DataType, + { + self.tds.validate()?; + self.validate_global_connectedness()?; + let facet_to_simplices: FacetToSimplicesMap = self.tds.build_facet_to_simplices_map()?; + self.validate_topology_core_with_facet_to_simplices_map(&facet_to_simplices)?; + // Check geometric orientation after manifold/link checks so topology-specific + // diagnostics surface first when multiple invariants are violated. + self.validate_geometric_simplex_orientation()?; + self.validate_at_completion_with_facet_to_simplices_map(&facet_to_simplices) + } + + /// Generate a comprehensive validation report for Levels 1–3. + /// + /// This is intended for debugging/telemetry where you want to see *all* violated + /// invariants, not just the first one. + /// + /// # Notes + /// - If UUID↔key mappings are inconsistent, this returns only mapping failures (other + /// checks may produce misleading secondary errors). + /// - This report is **cumulative** across Levels 1–3. + /// + /// # Errors + /// + /// Returns `Err(TriangulationValidationReport)` containing all invariant violations. + pub(crate) fn validation_report(&self) -> Result<(), TriangulationValidationReport> + where + U: DataType, + V: DataType, + { + let mut violations: Vec = Vec::new(); + + // Level 2 (structural): reuse the TDS report. + match self.tds.validation_report() { + Ok(()) => {} + Err(report) => { + if report.violations.iter().any(|v| { + matches!( + v.kind, + InvariantKind::VertexMappings | InvariantKind::SimplexMappings + ) + }) { + return Err(report); + } + violations.extend(report.violations); + } + } + + // Level 1 (element validity): vertices + for (_vertex_key, vertex) in self.tds.vertices() { + if let Err(source) = (*vertex).is_valid() { + violations.push(InvariantViolation { + kind: InvariantKind::VertexValidity, + error: InvariantError::Tds(TdsError::InvalidVertex { + vertex_id: vertex.uuid(), + source, + }), + }); + } + } + + // Level 1 (element validity): simplices + for (_simplex_key, simplex) in self.tds.simplices() { + if let Err(source) = simplex.is_valid() { + violations.push(InvariantViolation { + kind: InvariantKind::SimplexValidity, + error: InvariantError::Tds(TdsError::InvalidSimplex { + simplex_id: simplex.uuid(), + source, + }), + }); + } + } + + // Level 3 (topology) + if let Err(e) = self.is_valid() { + violations.push(InvariantViolation { + kind: InvariantKind::Topology, + error: e, + }); + } + + if violations.is_empty() { + Ok(()) + } else { + Err(TriangulationValidationReport { violations }) + } + } + + /// Validates that the triangulation's simplex neighbor graph is a single connected component. + /// + /// Delegates to [`Tds::is_connected`](crate::core::tds::Tds::is_connected), an O(N·D) BFS + /// over neighbor pointers. + pub(crate) fn validate_global_connectedness(&self) -> Result<(), TriangulationValidationError> { + if !self.tds.is_connected() { + return Err(TriangulationValidationError::Disconnected { + simplex_count: self.tds.number_of_simplices(), + }); + } + Ok(()) + } + + /// Validates that every vertex is incident to at least one simplex. + /// + /// Isolated vertices are allowed at the TDS (structural) layer, but they violate the + /// manifold invariants checked at the topology (Level 3) layer. + pub(crate) fn validate_no_isolated_vertices(&self) -> Result<(), TriangulationValidationError> { + if self.tds.number_of_vertices() == 0 { + return Ok(()); + } + + let mut vertices_in_simplices: FastHashSet = + fast_hash_set_with_capacity(self.tds.number_of_vertices()); + + for (_simplex_key, simplex) in self.tds.simplices() { + for &vk in simplex.vertices() { + vertices_in_simplices.insert(vk); + } + } + + for (vk, vertex) in self.tds.vertices() { + if !vertices_in_simplices.contains(&vk) { + return Err(TriangulationValidationError::IsolatedVertex { + vertex_key: vk, + vertex_uuid: vertex.uuid(), + }); + } + } + + Ok(()) + } + + /// Convert an [`InvariantError`] into the appropriate [`InsertionError`] variant. + /// + /// - `InvariantError::Tds(e)` → `InsertionError::TopologyValidation(e)` + /// - `InvariantError::Triangulation(e)` → `InsertionError::TopologyValidationFailed { source: e }` + /// - `InvariantError::Delaunay(e)` → `InsertionError::DelaunayValidationFailed { message }` + pub(crate) fn invariant_error_to_insertion_error(err: InvariantError) -> InsertionError { + match err { + InvariantError::Tds(tds_err) => InsertionError::TopologyValidation(tds_err), + InvariantError::Triangulation(tri_err) => InsertionError::TopologyValidationFailed { + message: "Topology validation failed".to_string(), + source: tri_err, + }, + InvariantError::Delaunay(dt_err) => { + InsertionError::DelaunayValidationFailed { source: dt_err } + } + } + } + + /// Runs mandatory link checks required by the topology guarantee. + pub(crate) fn validate_required_topology_links(&self) -> Result<(), InvariantError> { + if self.tds.number_of_simplices() == 0 { + return Ok(()); + } + + let facet_to_simplices: FacetToSimplicesMap = self.tds.build_facet_to_simplices_map()?; + validate_facet_degree(&facet_to_simplices)?; + validate_closed_boundary(&self.tds, &facet_to_simplices)?; + + if self.topology_guarantee.requires_ridge_links() { + validate_ridge_links(&self.tds)?; + } + + if self + .topology_guarantee + .requires_vertex_links_during_insertion() + { + validate_vertex_links(&self.tds, &facet_to_simplices)?; + } + + // Keep geometric orientation non-negotiable during incremental insertion, + // even when global validation is throttled. Run this after topology + // checks so topology diagnostics still surface first. + self.validate_geometric_simplex_orientation()?; + + Ok(()) + } + + /// Runs the localized connectedness guard after insertion. + /// + /// This checks that surviving new simplices form one component and, when + /// older simplices exist, that the new component attaches back to them via + /// mutual neighbor pointers. + pub(crate) fn validate_connectedness( + &self, + new_simplices: &SimplexKeyBuffer, + ) -> Result<(), InsertionError> { + let total_simplices = self.tds.number_of_simplices(); + if total_simplices == 0 { + return Ok(()); + } + + let mut new_set: SimplexKeySet = SimplexKeySet::default(); + new_set.reserve(new_simplices.len()); + for &ck in new_simplices { + if self.tds.contains_simplex(ck) { + new_set.insert(ck); + } + } + + if new_set.is_empty() { + return Err(InsertionError::TopologyValidation( + TdsError::InconsistentDataStructure { + message: "Disconnected triangulation detected after insertion: no surviving new simplices" + .to_string(), + }, + )); + } + + let expected_new_simplices = new_set.len(); + + let Some(&start) = new_set.iter().next() else { + return Err(InsertionError::TopologyValidation( + TdsError::InconsistentDataStructure { + message: + "new_set unexpectedly empty after non-empty check in validate_connectedness" + .to_string(), + }, + )); + }; + + let mut touches_existing_simplices = false; + + let visited = self.traverse_simplex_neighbor_graph( + start, + expected_new_simplices, + Some(&new_set), + |ck, nk| { + if touches_existing_simplices { + return; + } + + // For connectivity between new simplices and existing simplices, require *mutual* adjacency. + // This avoids treating one-way neighbor pointers as “connected”. + if let Some(neighbor_simplex) = self.tds.simplex(nk) + && neighbor_simplex + .neighbor_keys() + .is_some_and(|mut neighbor_keys| { + neighbor_keys.any(|neighbor| neighbor == Some(ck)) + }) + { + touches_existing_simplices = true; + } + }, + ); + + if visited.len() != expected_new_simplices { + return Err(InsertionError::TopologyValidation( + TdsError::InconsistentDataStructure { + message: format!( + "Disconnected triangulation detected after insertion: new-simplex subgraph visited {} of {} simplices", + visited.len(), + expected_new_simplices + ), + }, + )); + } + + if total_simplices > expected_new_simplices && !touches_existing_simplices { + return Err(InsertionError::TopologyValidation( + TdsError::InconsistentDataStructure { + message: format!( + "Disconnected triangulation detected after insertion: new-simplex component ({expected_new_simplices} simplices) is not connected to existing simplices (total_simplices={total_simplices})" + ), + }, + )); + } + + Ok(()) + } + + /// Runs mandatory topology checks over the local simplices touched by insertion. + /// + /// Soundness boundary: the scoped path checks coherent orientation, local + /// pseudomanifold facet incidence, ridge links, and geometric simplex + /// orientation. Those local checks are sufficient only when `simplices` is + /// non-empty and `topology_guarantee` does not require vertex-link checks + /// during insertion; otherwise this explicitly falls back to + /// [`validate_required_topology_links`](Self::validate_required_topology_links). + /// See `REFERENCES.md`, "Scoped Local Validation and Flips" \[1\], for the + /// local-vs-global validation tradeoff and geometric conditioning context. + pub(crate) fn validate_required_topology_links_for_simplices( + &self, + simplices: &[SimplexKey], + ) -> Result<(), InvariantError> { + if self.tds.number_of_simplices() == 0 { + return Ok(()); + } + + if simplices.is_empty() + || self + .topology_guarantee + .requires_vertex_links_during_insertion() + { + return self.validate_required_topology_links(); + } + + self.tds + .validate_coherent_orientation_for_simplices(simplices)?; + validate_local_pseudomanifold_for_simplices(&self.tds, simplices)?; + + if self.topology_guarantee.requires_ridge_links() { + validate_ridge_links_for_simplices(&self.tds, simplices)?; + } + + self.validate_geometric_simplex_orientation_for_simplices(simplices)?; + + Ok(()) + } + + pub(crate) fn validation_after_insertion_work( + &self, + suspicion: SuspicionFlags, + ) -> Option { + if self.tds.number_of_simplices() == 0 { + return None; + } + + let should_validate = self.validation_policy.should_validate(suspicion); + let requires_required_topology_checks = self + .topology_guarantee + .requires_pseudomanifold_checks_during_insertion(); + + if should_validate { + Some(InsertionValidationWork::FullValidation) + } else if requires_required_topology_checks { + Some(InsertionValidationWork::RequiredTopologyLinks) + } else { + None + } + } + + pub(crate) fn validate_after_insertion_with_scope( + &self, + suspicion: SuspicionFlags, + local_simplices: Option<&[SimplexKey]>, + ) -> Result<(), InvariantError> { + let Some(work) = self.validation_after_insertion_work(suspicion) else { + return Ok(()); + }; + + log_validation_trigger_if_enabled(self.validation_policy, suspicion); + match work { + InsertionValidationWork::FullValidation => self.is_valid(), + InsertionValidationWork::RequiredTopologyLinks => local_simplices.map_or_else( + || self.validate_required_topology_links(), + |simplices| self.validate_required_topology_links_for_simplices(simplices), + ), + } + } + + /// Runs post-insertion validation and records count/timing telemetry for the selected work. + pub(crate) fn validate_after_insertion_and_record_telemetry( + &self, + suspicion: SuspicionFlags, + local_simplices: &[SimplexKey], + telemetry: &mut InsertionTelemetry, + telemetry_mode: InsertionTelemetryMode, + ) -> Result<(), InvariantError> { + let validation_work = self.validation_after_insertion_work(suspicion); + let validation_started = + validation_work.and_then(|_| start_insertion_timing(telemetry_mode)); + let validation_result = + self.validate_after_insertion_with_scope(suspicion, Some(local_simplices)); + + if validation_work.is_some() { + record_topology_validation_telemetry( + telemetry, + validation_started.map(|started| duration_nanos_saturating(started.elapsed())), + ); + } + + validation_result + } +} + +/// Logs when Level 3 validation is triggered (debug builds only). +#[inline] +fn log_validation_trigger_if_enabled(policy: ValidationPolicy, suspicion: SuspicionFlags) { + #[cfg(debug_assertions)] + if policy.should_validate(suspicion) && suspicion.is_suspicious() { + tracing::debug!("Validation triggered by {suspicion:?}"); + } + + // Keep the parameters "used" in release builds where the debug-only logging + // is compiled out, so `cargo clippy -D warnings` stays clean across profiles. + #[cfg(not(debug_assertions))] + { + let _ = policy; + let _ = suspicion; + } +} + +/// Records one topology-validation pass and its optional elapsed time. +#[inline] +fn record_topology_validation_telemetry( + telemetry: &mut InsertionTelemetry, + elapsed_nanos: Option, +) { + telemetry.topology_validation_calls = telemetry.topology_validation_calls.saturating_add(1); + if let Some(elapsed_nanos) = elapsed_nanos { + telemetry.topology_validation_nanos = telemetry + .topology_validation_nanos + .saturating_add(elapsed_nanos); + telemetry.topology_validation_nanos_max = + telemetry.topology_validation_nanos_max.max(elapsed_nanos); + } +} + +/// Convert a duration to nanoseconds while saturating at `u64::MAX`. +#[inline] +fn duration_nanos_saturating(duration: std::time::Duration) -> u64 { + u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX) +} + +/// Starts a wall-clock timer only when insertion telemetry will publish timings. +#[inline] +fn start_insertion_timing(telemetry_mode: InsertionTelemetryMode) -> Option { + telemetry_mode.records_timings().then(Instant::now) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::algorithms::incremental_insertion::CavityFillingError; + use crate::core::algorithms::incremental_insertion::repair_neighbor_pointers; + use crate::core::collections::NeighborBuffer; + use crate::core::operations::InsertionOutcome; + use crate::core::simplex::Simplex; + use crate::core::tds::{GeometricError, NeighborValidationError, Tds}; + use crate::core::vertex::Vertex; + use crate::core::vertex::VertexBuilder; + use crate::geometry::kernel::FastKernel; + use crate::geometry::util::generate_random_points_seeded; + use crate::repair::DelaunayRepairPolicy; + use crate::triangulation::DelaunayTriangulation; + use crate::validation::DelaunayTriangulationValidationError; + use crate::vertex; + use slotmap::KeyData; + + fn insert_test_vertex_with_coords( + tds: &mut Tds, + entries: &[(usize, f64)], + ) -> VertexKey { + let mut coords = [0.0_f64; D]; + for &(axis, value) in entries { + coords[axis] = value; + } + tds.insert_vertex_with_mapping(vertex!(coords)).unwrap() + } + + fn build_invalid_vertex_link_tds() -> (Tds, VertexKey) { + let mut tds: Tds = Tds::empty(); + let shared = insert_test_vertex_with_coords(&mut tds, &[]); + + if D == 2 { + let first_a = insert_test_vertex_with_coords(&mut tds, &[(0, 1.0)]); + let first_b = insert_test_vertex_with_coords(&mut tds, &[(1, 1.0)]); + let first_c = insert_test_vertex_with_coords(&mut tds, &[(0, -1.0)]); + let second_a = insert_test_vertex_with_coords(&mut tds, &[(0, 10.0)]); + let second_b = insert_test_vertex_with_coords(&mut tds, &[(0, 11.0), (1, 1.0)]); + let second_c = insert_test_vertex_with_coords(&mut tds, &[(0, 9.0), (1, 1.0)]); + + for simplex_vertices in [ + vec![shared, first_a, first_b], + vec![shared, first_b, first_c], + vec![shared, first_c, first_a], + vec![shared, second_a, second_b], + vec![shared, second_b, second_c], + vec![shared, second_c, second_a], + ] { + let _ = tds + .insert_simplex_with_mapping(Simplex::new(simplex_vertices, None).unwrap()) + .unwrap(); + } + + tds.assign_incident_simplices().unwrap(); + return (tds, shared); + } + + let mut first_simplex_vertices = vec![shared]; + for axis in 0..D { + let mut coords = [0.0_f64; D]; + coords[axis] = 1.0; + first_simplex_vertices.push(tds.insert_vertex_with_mapping(vertex!(coords)).unwrap()); + } + + let mut second_simplex_vertices = vec![shared]; + for axis in 0..D { + let mut coords = [0.0_f64; D]; + coords[0] = 10.0; + coords[axis] += 1.0; + second_simplex_vertices.push(tds.insert_vertex_with_mapping(vertex!(coords)).unwrap()); + } + + let _ = tds + .insert_simplex_with_mapping(Simplex::new(first_simplex_vertices, None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(second_simplex_vertices, None).unwrap()) + .unwrap(); + + tds.assign_incident_simplices().unwrap(); + + (tds, shared) + } + + fn build_invalid_vertex_link_tds_2d() -> (Tds, VertexKey) { + build_invalid_vertex_link_tds::<2>() + } + + fn build_disconnected_two_triangles_tds_2d() -> Tds { + let mut tds: Tds = Tds::empty(); + + let a0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let a1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); + let a2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); + + let b0 = tds + .insert_vertex_with_mapping(vertex!([10.0, 0.0])) + .unwrap(); + let b1 = tds + .insert_vertex_with_mapping(vertex!([11.0, 0.0])) + .unwrap(); + let b2 = tds + .insert_vertex_with_mapping(vertex!([10.0, 1.0])) + .unwrap(); + + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![a0, a1, a2], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![b0, b1, b2], None).unwrap()) + .unwrap(); + + tds + } + + fn build_three_triangles_sharing_edge_tds_2d() -> Tds { + let mut tds: Tds = Tds::empty(); + + let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); + let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); + let v3 = tds + .insert_vertex_with_mapping(vertex!([0.0, -1.0])) + .unwrap(); + let v4 = tds.insert_vertex_with_mapping(vertex!([2.0, 0.0])).unwrap(); + + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v3], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_bypassing_topology_checks_for_test( + Simplex::new(vec![v0, v1, v4], None).unwrap(), + ) + .unwrap(); + + tds + } + + fn unit_simplex_vertices() -> Vec> { + let mut vertices = Vec::with_capacity(D + 1); + vertices.push(vertex!([0.0_f64; D])); + for axis in 0..D { + let mut coords = [0.0_f64; D]; + coords[axis] = 1.0; + vertices.push(vertex!(coords)); + } + vertices + } + + fn unit_simplex_interior_vertex() -> Vertex { + vertex!([0.125_f64; D]) + } + + fn build_single_tet() -> ( + Triangulation, (), (), 3>, + [VertexKey; 4], + SimplexKey, + ) { + let mut tds: Tds = Tds::empty(); + let v0 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) + .unwrap(); + let v1 = tds + .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) + .unwrap(); + let v2 = tds + .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) + .unwrap(); + let v3 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) + .unwrap(); + let ck = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap()) + .unwrap(); + for vk in [v0, v1, v2, v3] { + tds.vertex_mut(vk).unwrap().set_incident_simplex(Some(ck)); + } + + ( + Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds), + [v0, v1, v2, v3], + ck, + ) + } + + #[test] + fn triangulation_validation_error_try_from_manifold_error_preserves_detail() { + let tds_err = TdsError::InvalidNeighbors { + reason: NeighborValidationError::Other { + message: "unit test".to_string(), + }, + }; + + assert_eq!( + TriangulationValidationError::try_from(ManifoldError::Tds(tds_err.clone())), + Err(tds_err.clone()) + ); + assert_eq!( + InvariantError::from(ManifoldError::Tds(tds_err.clone())), + InvariantError::Tds(tds_err) + ); + + assert!(matches!( + TriangulationValidationError::try_from(ManifoldError::ManifoldFacetMultiplicity { + facet_key: 123, + simplex_count: 3 + }) + .unwrap(), + TriangulationValidationError::ManifoldFacetMultiplicity { + facet_key: 123, + simplex_count: 3 + } + )); + + assert!(matches!( + TriangulationValidationError::try_from(ManifoldError::BoundaryRidgeMultiplicity { + ridge_key: 0x00ab_cdef, + boundary_facet_count: 4 + }) + .unwrap(), + TriangulationValidationError::BoundaryRidgeMultiplicity { + ridge_key: 0x00ab_cdef, + boundary_facet_count: 4 + } + )); + + assert!(matches!( + TriangulationValidationError::try_from(ManifoldError::RidgeLinkNotManifold { + ridge_key: 0x00ab_cdef, + link_vertex_count: 7, + link_edge_count: 8, + max_degree: 3, + degree_one_vertices: 2, + connected: false + }) + .unwrap(), + TriangulationValidationError::RidgeLinkNotManifold { + ridge_key: 0x00ab_cdef, + link_vertex_count: 7, + link_edge_count: 8, + max_degree: 3, + degree_one_vertices: 2, + connected: false + } + )); + + assert!(matches!( + TriangulationValidationError::try_from(ManifoldError::VertexLinkNotManifold { + vertex_key: VertexKey::from(KeyData::from_ffi(1)), + link_vertex_count: 3, + link_simplex_count: 4, + boundary_facet_count: 1, + max_degree: 2, + connected: false, + interior_vertex: true, + }) + .unwrap(), + TriangulationValidationError::VertexLinkNotManifold { + link_vertex_count: 3, + link_simplex_count: 4, + boundary_facet_count: 1, + max_degree: 2, + connected: false, + interior_vertex: true, + .. + } + )); + } + + #[test] + fn validation_policy_should_validate_matrix() { + let clean = SuspicionFlags::default(); + let suspicious = SuspicionFlags { + perturbation_used: true, + ..SuspicionFlags::default() + }; + + assert!(!ValidationPolicy::Never.should_validate(clean)); + assert!(!ValidationPolicy::Never.should_validate(suspicious)); + assert!(ValidationPolicy::Always.should_validate(clean)); + assert!(ValidationPolicy::Always.should_validate(suspicious)); + assert!(!ValidationPolicy::OnSuspicion.should_validate(clean)); + assert!(ValidationPolicy::OnSuspicion.should_validate(suspicious)); + assert!(ValidationPolicy::DebugOnly.should_validate(suspicious)); + assert_eq!( + ValidationPolicy::DebugOnly.should_validate(clean), + cfg!(debug_assertions) + ); + } + + #[test] + fn topology_guarantee_helper_matrix_and_policy_compatibility() { + assert_eq!(TopologyGuarantee::default(), TopologyGuarantee::DEFAULT); + assert_eq!(TopologyGuarantee::DEFAULT, TopologyGuarantee::PLManifold); + assert!(!TopologyGuarantee::Pseudomanifold.requires_vertex_links_during_insertion()); + assert!(TopologyGuarantee::PLManifoldStrict.requires_vertex_links_during_insertion()); + assert!(!TopologyGuarantee::Pseudomanifold.requires_vertex_links_at_completion()); + assert!(TopologyGuarantee::PLManifold.requires_vertex_links_at_completion()); + assert!(TopologyGuarantee::PLManifoldStrict.requires_vertex_links_at_completion()); + assert!( + TopologyGuarantee::Pseudomanifold.requires_pseudomanifold_checks_during_insertion() + ); + assert!(TopologyGuarantee::PLManifold.requires_pseudomanifold_checks_during_insertion()); + assert!( + TopologyGuarantee::PLManifoldStrict.requires_pseudomanifold_checks_during_insertion() + ); + assert!(!TopologyGuarantee::Pseudomanifold.requires_ridge_links()); + assert!(TopologyGuarantee::PLManifold.requires_ridge_links()); + assert!(TopologyGuarantee::PLManifoldStrict.requires_ridge_links()); + + assert_eq!( + TopologyGuarantee::PLManifoldStrict.default_validation_policy(), + ValidationPolicy::Always + ); + assert_eq!( + TopologyGuarantee::PLManifold.default_validation_policy(), + ValidationPolicy::OnSuspicion + ); + assert_eq!( + TopologyGuarantee::Pseudomanifold.default_validation_policy(), + ValidationPolicy::OnSuspicion + ); + + let tri = Triangulation::, (), (), 2>::new_empty(FastKernel::new()); + assert_eq!( + tri.validation_policy(), + TopologyGuarantee::DEFAULT.default_validation_policy() + ); + + for policy in [ + ValidationPolicy::Never, + ValidationPolicy::OnSuspicion, + ValidationPolicy::Always, + ValidationPolicy::DebugOnly, + ] { + assert!(TopologyGuarantee::Pseudomanifold.is_compatible_with_policy(policy)); + } + + assert!(!TopologyGuarantee::PLManifold.is_compatible_with_policy(ValidationPolicy::Never)); + assert!( + !TopologyGuarantee::PLManifoldStrict.is_compatible_with_policy(ValidationPolicy::Never) + ); + assert!( + TopologyGuarantee::PLManifold.is_compatible_with_policy(ValidationPolicy::OnSuspicion) + ); + assert!(TopologyGuarantee::PLManifold.is_compatible_with_policy(ValidationPolicy::Always)); + assert!( + TopologyGuarantee::PLManifoldStrict.is_compatible_with_policy(ValidationPolicy::Always) + ); + } + + #[test] + fn validation_accessors_and_mutators_round_trip() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + assert_eq!(tri.topology_guarantee(), TopologyGuarantee::PLManifold); + assert_eq!(tri.validation_policy(), ValidationPolicy::OnSuspicion); + + tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + tri.set_validation_policy(ValidationPolicy::Always); + + assert_eq!(tri.topology_guarantee(), TopologyGuarantee::Pseudomanifold); + assert_eq!(tri.validation_policy(), ValidationPolicy::Always); + } + + #[test] + fn incompatible_policy_updates_when_completion_validation_succeeds() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + assert_eq!(tri.topology_guarantee(), TopologyGuarantee::PLManifold); + assert_eq!(tri.validation_policy(), ValidationPolicy::OnSuspicion); + + tri.set_validation_policy(ValidationPolicy::Never); + assert_eq!(tri.validation_policy(), ValidationPolicy::Never); + } + + #[test] + fn incompatible_guarantee_updates_when_completion_validation_succeeds() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + tri.set_validation_policy(ValidationPolicy::Never); + assert_eq!(tri.validation_policy(), ValidationPolicy::Never); + assert_eq!(tri.topology_guarantee(), TopologyGuarantee::PLManifold); + + tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); + assert_eq!( + tri.topology_guarantee(), + TopologyGuarantee::PLManifoldStrict + ); + } + + #[test] + fn validate_at_completion_skips_for_pseudomanifold() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + assert!(tri.validate_at_completion().is_ok()); + } + + #[test] + fn validate_at_completion_reports_invalid_vertex_link() { + let (tds, v0) = build_invalid_vertex_link_tds_2d(); + + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + tri.set_topology_guarantee(TopologyGuarantee::PLManifold); + + match tri.validate_at_completion() { + Err(InvariantError::Triangulation( + TriangulationValidationError::VertexLinkNotManifold { vertex_key, .. }, + )) => assert_eq!(vertex_key, v0), + other => panic!("Expected VertexLinkNotManifold, got {other:?}"), + } + } + + #[test] + fn incompatible_policy_rejected_when_completion_validation_fails() { + let (tds, _) = build_invalid_vertex_link_tds_2d(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + assert!(matches!( + tri.validate_at_completion(), + Err(InvariantError::Triangulation( + TriangulationValidationError::VertexLinkNotManifold { .. } + )) + )); + assert_eq!(tri.validation_policy(), ValidationPolicy::OnSuspicion); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + tri.set_validation_policy(ValidationPolicy::Never); + })); + if cfg!(debug_assertions) { + assert!(result.is_err()); + } else { + assert!(result.is_ok()); + } + assert_eq!(tri.validation_policy(), ValidationPolicy::OnSuspicion); + } + + #[test] + fn incompatible_guarantee_rejected_when_completion_validation_fails() { + let (tds, _) = build_invalid_vertex_link_tds_2d(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + tri.set_validation_policy(ValidationPolicy::Never); + assert_eq!(tri.topology_guarantee(), TopologyGuarantee::Pseudomanifold); + assert_eq!(tri.validation_policy(), ValidationPolicy::Never); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); + })); + if cfg!(debug_assertions) { + assert!(result.is_err()); + } else { + assert!(result.is_ok()); + } + assert_eq!(tri.topology_guarantee(), TopologyGuarantee::Pseudomanifold); + } + + #[test] + fn validate_after_insertion_skips_when_no_simplices() { + let mut tri: Triangulation, (), (), 2> = + Triangulation::new_empty(FastKernel::new()); + + tri.set_validation_policy(ValidationPolicy::Always); + let _ = tri + .tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0])) + .unwrap(); + assert_eq!(tri.number_of_simplices(), 0); + + tri.validate_after_insertion_with_scope(SuspicionFlags::default(), None) + .unwrap(); + } + + #[test] + fn validate_after_insertion_calls_is_valid_when_policy_triggers() { + let tds = build_disconnected_two_triangles_tds_2d(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + tri.set_validation_policy(ValidationPolicy::Always); + + match tri.validate_after_insertion_with_scope(SuspicionFlags::default(), None) { + Err(InvariantError::Triangulation(TriangulationValidationError::Disconnected { + .. + })) => {} + other => panic!("Expected Disconnected error, got {other:?}"), + } + } + + #[test] + fn validation_after_insertion_work_matches_policy_and_link_requirements() { + let tds = build_disconnected_two_triangles_tds_2d(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + tri.set_validation_policy(ValidationPolicy::OnSuspicion); + tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + assert_eq!( + tri.validation_after_insertion_work(SuspicionFlags::default()), + Some(InsertionValidationWork::RequiredTopologyLinks) + ); + + tri.set_topology_guarantee(TopologyGuarantee::PLManifold); + assert_eq!( + tri.validation_after_insertion_work(SuspicionFlags::default()), + Some(InsertionValidationWork::RequiredTopologyLinks) + ); + + tri.set_validation_policy(ValidationPolicy::Always); + assert_eq!( + tri.validation_after_insertion_work(SuspicionFlags::default()), + Some(InsertionValidationWork::FullValidation) + ); + } + + #[test] + fn insertion_error_to_invariant_error_maps_all_arms() { + let source = TdsError::Geometric(GeometricError::DegenerateOrientation { + message: "det=0".to_string(), + }); + let error = InsertionError::TopologyValidation(source.clone()); + assert_eq!( + insertion_error_to_invariant_error(error, "ctx"), + InvariantError::Tds(source) + ); + + let inner = TriangulationValidationError::IsolatedVertex { + vertex_key: VertexKey::from(KeyData::from_ffi(1)), + vertex_uuid: Uuid::nil(), + }; + let error = InsertionError::TopologyValidationFailed { + message: "outer".to_string(), + source: inner.clone(), + }; + assert_eq!( + insertion_error_to_invariant_error(error, "ctx"), + InvariantError::Triangulation(inner) + ); + + let error = InsertionError::CavityFilling { + reason: CavityFillingError::EmptyFanTriangulation, + }; + let result = insertion_error_to_invariant_error(error, "ctx"); + assert!( + matches!( + result, + InvariantError::Tds(TdsError::InconsistentDataStructure { ref message }) + if message.contains("ctx") && message.contains("fan triangulation produced no simplices") + ), + "CavityFilling should wrap to InconsistentDataStructure: {result:?}" + ); + } + + #[test] + fn invariant_error_to_insertion_error_maps_all_arms() { + let inv = InvariantError::Tds(TdsError::InconsistentDataStructure { + message: "test".to_string(), + }); + let ins = + Triangulation::, (), (), 3>::invariant_error_to_insertion_error(inv); + assert!(matches!(ins, InsertionError::TopologyValidation(_))); + + let inv = InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { + vertex_key: VertexKey::from(KeyData::from_ffi(1)), + vertex_uuid: Uuid::nil(), + }); + let ins = + Triangulation::, (), (), 3>::invariant_error_to_insertion_error(inv); + assert!(matches!( + ins, + InsertionError::TopologyValidationFailed { .. } + )); + + let inv = + InvariantError::Delaunay(DelaunayTriangulationValidationError::VerificationFailed { + message: "test".to_string(), + }); + let ins = + Triangulation::, (), (), 3>::invariant_error_to_insertion_error(inv); + assert!(matches!( + ins, + InsertionError::DelaunayValidationFailed { .. } + )); + } + + #[test] + fn from_manifold_error_routes_tds_and_topology_layers() { + let tds_err = TdsError::InconsistentDataStructure { + message: "underlying TDS issue".to_string(), + }; + let manifold_err = ManifoldError::Tds(tds_err.clone()); + assert_eq!( + InvariantError::from(manifold_err), + InvariantError::Tds(tds_err) + ); + + let err = ManifoldError::ManifoldFacetMultiplicity { + facet_key: 999, + simplex_count: 5, + }; + let inv = InvariantError::from(err); + assert!(matches!( + inv, + InvariantError::Triangulation( + TriangulationValidationError::ManifoldFacetMultiplicity { + facet_key: 999, + simplex_count: 5 + } + ) + )); + } + + #[test] + fn isolated_vertex_error_display_is_informative() { + let err = TriangulationValidationError::IsolatedVertex { + vertex_key: VertexKey::from(KeyData::from_ffi(42)), + vertex_uuid: Uuid::nil(), + }; + let msg = err.to_string(); + assert!(msg.contains("Isolated vertex")); + assert!(msg.contains("not incident to any simplex")); + } + + #[test] + fn is_valid_returns_triangulation_error_for_isolated_vertex() { + let (mut tri, _, _) = build_single_tet(); + let iso = tri + .tds + .insert_vertex_with_mapping(vertex!([0.5, 0.5, 0.5])) + .unwrap(); + + match tri.is_valid() { + Err(InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { + vertex_key, + .. + })) => assert_eq!(vertex_key, iso), + other => { + panic!("Expected InvariantError::Triangulation(IsolatedVertex), got {other:?}") + } + } + } + + #[test] + fn is_valid_returns_triangulation_error_for_disconnected() { + let tds = build_disconnected_two_triangles_tds_2d(); + let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + match tri.is_valid() { + Err(InvariantError::Triangulation(TriangulationValidationError::Disconnected { + simplex_count, + })) => assert_eq!(simplex_count, 2), + other => { + panic!("Expected InvariantError::Triangulation(Disconnected), got {other:?}") + } + } + } + + #[test] + fn validate_returns_invariant_error_from_tds_layer() { + let (mut tri, [v0, _, _, _], _) = build_single_tet(); + let uuid = tri.tds.vertex(v0).unwrap().uuid(); + tri.tds.uuid_to_vertex_key.remove(&uuid); + + match tri.validate() { + Err(InvariantError::Tds(TdsError::MappingInconsistency { .. })) => {} + other => panic!("Expected InvariantError::Tds(MappingInconsistency), got {other:?}"), + } + } + + #[test] + fn validate_returns_invariant_error_from_topology_layer() { + let (mut tri, _, _) = build_single_tet(); + let _ = tri + .tds + .insert_vertex_with_mapping(vertex!([0.5, 0.5, 0.5])) + .unwrap(); + + match tri.validate() { + Err(InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { + .. + })) => {} + other => { + panic!("Expected InvariantError::Triangulation(IsolatedVertex), got {other:?}") + } + } + } + + #[test] + fn validation_report_ok_for_valid_triangulation() { + let vertices = [ + 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]), + vertex!([0.5, 0.5, 0.5]), + ]; + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + assert!(dt.as_triangulation().validation_report().is_ok()); + } + + #[test] + fn validation_report_reports_isolated_vertex_topology_violation() { + let (mut tri, _, _) = build_single_tet(); + let _ = tri + .tds + .insert_vertex_with_mapping(vertex!([0.5, 0.5, 0.5])) + .unwrap(); + + let report = tri.validation_report().unwrap_err(); + assert!(!report.is_empty()); + assert!( + report + .violations + .iter() + .any(|v| v.kind == InvariantKind::Topology), + "Expected Topology violation in report" + ); + } + + #[test] + fn validate_global_connectedness_ok_for_connected() { + let (tri, _, _) = build_single_tet(); + assert!(tri.validate_global_connectedness().is_ok()); + } + + #[test] + fn validate_no_isolated_vertices_ok_when_no_vertices() { + let tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + assert!(tri.validate_no_isolated_vertices().is_ok()); + } + + macro_rules! test_is_valid_topology { + ($dim:expr, [$($simplex_coords:expr),+ $(,)?]) => { + pastey::paste! { + #[test] + fn []() { + let vertices: Vec> = vec![ + $(vertex!($simplex_coords)),+ + ]; + + let expected_vertices = vertices.len(); + assert_eq!(expected_vertices, $dim + 1); + + let dt = DelaunayTriangulation::new(&vertices) + .expect("simplex construction should succeed"); + let tri = dt.as_triangulation(); + + assert!(tri.is_valid().is_ok()); + assert_eq!(tri.number_of_vertices(), expected_vertices); + assert_eq!(tri.number_of_simplices(), 1); + } + } + }; + } + + test_is_valid_topology!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]]); + test_is_valid_topology!( + 3, + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0] + ] + ); + test_is_valid_topology!( + 4, + [ + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0] + ] + ); + test_is_valid_topology!( + 5, + [ + [0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0] + ] + ); + + #[test] + fn is_valid_topology_empty() { + let tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + assert!(tri.is_valid().is_ok()); + } + + #[test] + fn is_valid_pl_manifold_mode_rejects_wedge_at_vertex_in_2d() { + let mut tds: Tds = Tds::empty(); + + let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); + let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); + let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap(); + + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v3], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v2, v3], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v1, v2, v3], None).unwrap()) + .unwrap(); + + let v4 = tds + .insert_vertex_with_mapping(vertex!([10.0, 10.0])) + .unwrap(); + let v5 = tds + .insert_vertex_with_mapping(vertex!([11.0, 10.0])) + .unwrap(); + let v6 = tds + .insert_vertex_with_mapping(vertex!([10.0, 11.0])) + .unwrap(); + + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v4, v5], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v4, v6], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v5, v6], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v4, v5, v6], None).unwrap()) + .unwrap(); + + repair_neighbor_pointers(&mut tds).unwrap(); + + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + + assert!(matches!( + tri.is_valid(), + Err(InvariantError::Triangulation( + TriangulationValidationError::Disconnected { .. } + )) + )); + + tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); + + match tri.is_valid() { + Err(InvariantError::Triangulation( + TriangulationValidationError::VertexLinkNotManifold { vertex_key, .. }, + )) => assert_eq!(vertex_key, v0), + Err(InvariantError::Triangulation( + TriangulationValidationError::RidgeLinkNotManifold { .. } + | TriangulationValidationError::Disconnected { .. }, + )) => {} + other => panic!( + "Expected RidgeLinkNotManifold, VertexLinkNotManifold, or Disconnected, got {other:?}" + ), + } + } + + #[test] + fn is_valid_pl_manifold_mode_rejects_cone_on_torus_in_3d_even_when_connected() { + const N: usize = 3; + const M: usize = 3; + + let mut tds: Tds = Tds::empty(); + let mut v: [[VertexKey; M]; N] = [[VertexKey::from(KeyData::from_ffi(0)); M]; N]; + for (i, row) in v.iter_mut().enumerate() { + for (j, slot) in row.iter_mut().enumerate() { + let i_f = >::from(u32::try_from(i).unwrap()); + let j_f = >::from(u32::try_from(j).unwrap()); + *slot = tds + .insert_vertex_with_mapping(vertex!([i_f, j_f, 0.0])) + .unwrap(); + } + } + + let apex = tds + .insert_vertex_with_mapping(vertex!([0.5, 0.5, 1.0])) + .unwrap(); + + for i in 0..N { + for j in 0..M { + let i1 = (i + 1) % N; + let j1 = (j + 1) % M; + let v00 = v[i][j]; + let v10 = v[i1][j]; + let v01 = v[i][j1]; + let v11 = v[i1][j1]; + + for tri in [[v00, v10, v01], [v10, v11, v01]] { + let _ = tds + .insert_simplex_with_mapping( + Simplex::new(vec![tri[0], tri[1], tri[2], apex], None).unwrap(), + ) + .unwrap(); + } + } + } + + repair_neighbor_pointers(&mut tds).unwrap(); + + let mut tri = + Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); + tri.validate_global_connectedness().unwrap(); + let facet_to_simplices = tri.tds.build_facet_to_simplices_map().unwrap(); + validate_facet_degree(&facet_to_simplices).unwrap(); + validate_closed_boundary(&tri.tds, &facet_to_simplices).unwrap(); + + tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); + + match tri.is_valid() { + Err(InvariantError::Triangulation( + TriangulationValidationError::VertexLinkNotManifold { + vertex_key, + connected, + interior_vertex, + .. + }, + )) => { + assert_eq!(vertex_key, apex); + assert!(connected); + assert!(interior_vertex); + } + other => panic!("Expected VertexLinkNotManifold for cone apex, got {other:?}"), + } + } + + #[test] + fn is_valid_disconnected_detected_before_non_manifold_boundary_ridge() { + let mut tds: Tds = Tds::empty(); + + let shared_edge_v0 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) + .unwrap(); + let shared_edge_v1 = tds + .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) + .unwrap(); + let tet1_v2 = tds + .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) + .unwrap(); + let tet1_v3 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) + .unwrap(); + let tet2_v2 = tds + .insert_vertex_with_mapping(vertex!([0.0, -1.0, 0.0])) + .unwrap(); + let tet2_v3 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, -1.0])) + .unwrap(); + + let _ = tds + .insert_simplex_with_mapping( + Simplex::new(vec![shared_edge_v0, shared_edge_v1, tet1_v2, tet1_v3], None).unwrap(), + ) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping( + Simplex::new(vec![shared_edge_v0, shared_edge_v1, tet2_v2, tet2_v3], None).unwrap(), + ) + .unwrap(); + + let tri = Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); + + match tri.is_valid() { + Err(InvariantError::Triangulation(TriangulationValidationError::Disconnected { + simplex_count, + })) => assert_eq!(simplex_count, 2), + other => panic!("Expected Disconnected, got {other:?}"), + } + } + + #[test] + fn validate_includes_tds_validation() { + 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::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + + assert!(tri.tds.validate().is_ok()); + assert!(tri.validate().is_ok()); + } + + #[test] + fn is_valid_rejects_bootstrap_phase_with_isolated_vertex() { + let mut tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + let vertex = vertex!([0.0, 0.0, 0.0]); + let expected_uuid = vertex.uuid(); + let expected_vk = tri.tds.insert_vertex_with_mapping(vertex).unwrap(); + + match tri.is_valid() { + Err(InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { + vertex_key, + vertex_uuid, + })) => { + assert_eq!(vertex_key, expected_vk); + assert_eq!(vertex_uuid, expected_uuid); + } + other => panic!("Expected IsolatedVertex error, got {other:?}"), + } + } + + #[test] + fn is_valid_rejects_isolated_vertex_even_when_simplices_exist() { + 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 tds = + Triangulation::, (), (), 3>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); + tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + + let _isolated_vk = tri + .tds + .insert_vertex_with_mapping(vertex!([10.0, 10.0, 10.0])) + .unwrap(); + + assert!(matches!( + tri.is_valid(), + Err(InvariantError::Triangulation( + TriangulationValidationError::IsolatedVertex { .. } + )) + )); + } + + #[test] + fn is_valid_rejects_disconnected_even_when_euler_matches() { + let mut tds: Tds = Tds::empty(); + + let v0 = tds.insert_vertex_with_mapping(vertex!([0.0])).unwrap(); + let v1 = tds.insert_vertex_with_mapping(vertex!([1.0])).unwrap(); + let v2 = tds.insert_vertex_with_mapping(vertex!([2.0])).unwrap(); + let e0 = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1], None).unwrap()) + .unwrap(); + let e1 = tds + .insert_simplex_with_mapping(Simplex::new(vec![v1, v2], None).unwrap()) + .unwrap(); + + let v3 = tds.insert_vertex_with_mapping(vertex!([10.0])).unwrap(); + let v4 = tds.insert_vertex_with_mapping(vertex!([11.0])).unwrap(); + let v5 = tds.insert_vertex_with_mapping(vertex!([12.0])).unwrap(); + let c0 = tds + .insert_simplex_with_mapping(Simplex::new(vec![v3, v4], None).unwrap()) + .unwrap(); + let c1 = tds + .insert_simplex_with_mapping(Simplex::new(vec![v4, v5], None).unwrap()) + .unwrap(); + let c2 = tds + .insert_simplex_with_mapping(Simplex::new(vec![v5, v3], None).unwrap()) + .unwrap(); + + for (simplex_key, neighbor_keys) in [ + (e0, [Some(e1), None]), + (e1, [None, Some(e0)]), + (c0, [Some(c1), Some(c2)]), + (c1, [Some(c2), Some(c0)]), + (c2, [Some(c0), Some(c1)]), + ] { + let simplex = tds.simplex_mut(simplex_key).unwrap(); + let mut neighbors = NeighborBuffer::>::new(); + neighbors.extend(neighbor_keys); + simplex.set_neighbors_from_keys(neighbors).unwrap(); + } + + tds.assign_incident_simplices().unwrap(); + + let tri = Triangulation::, (), (), 1>::new_with_tds(FastKernel::new(), tds); + let facet_to_simplices = tri.tds.build_facet_to_simplices_map().unwrap(); + validate_facet_degree(&facet_to_simplices).unwrap(); + + let topology = + validate_triangulation_euler_with_facet_to_simplices_map(&tri.tds, &facet_to_simplices); + assert_eq!(topology.classification, TopologyClassification::Ball(1)); + assert_eq!(topology.expected, Some(1)); + assert_eq!(topology.chi, 1); + + match tri.is_valid() { + Err(InvariantError::Triangulation(TriangulationValidationError::Disconnected { + simplex_count, + })) => assert_eq!(simplex_count, 5), + other => panic!("Expected Disconnected, got {other:?}"), + } + } + + #[test] + fn tds_is_valid_rejects_boundary_facet_has_neighbor() { + let vertices_simplex_1 = 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 mut tds = + Triangulation::, (), (), 3>::build_initial_simplex(&vertices_simplex_1) + .unwrap(); + let first_simplex_key = tds.simplex_keys().next().unwrap(); + + let v4 = tds + .insert_vertex_with_mapping(vertex!([10.0, 0.0, 0.0])) + .unwrap(); + let v5 = tds + .insert_vertex_with_mapping(vertex!([11.0, 0.0, 0.0])) + .unwrap(); + let v6 = tds + .insert_vertex_with_mapping(vertex!([10.0, 1.0, 0.0])) + .unwrap(); + let v7 = tds + .insert_vertex_with_mapping(vertex!([10.0, 0.0, 1.0])) + .unwrap(); + + let second_simplex_key = tds + .insert_simplex_with_mapping(Simplex::new(vec![v4, v5, v6, v7], None).unwrap()) + .unwrap(); + + let first_simplex = tds.simplex_mut(first_simplex_key).unwrap(); + let mut neighbors = NeighborBuffer::>::new(); + neighbors.resize(4, None); + neighbors[0] = Some(second_simplex_key); + first_simplex.set_neighbors_from_keys(neighbors).unwrap(); + + match tds.is_valid() { + Err(TdsError::InvalidNeighbors { + reason: NeighborValidationError::BoundaryFacetHasNeighbor { neighbor_key, .. }, + }) => assert_eq!(neighbor_key, second_simplex_key), + other => panic!("Expected InvalidNeighbors, got {other:?}"), + } + } + + #[test] + fn tds_is_valid_rejects_interior_facet_neighbor_mismatch() { + let mut tds: Tds = Tds::empty(); + + let v0 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) + .unwrap(); + let v1 = tds + .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) + .unwrap(); + let v2 = tds + .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) + .unwrap(); + let v3 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) + .unwrap(); + let v4 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 2.0])) + .unwrap(); + + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v4], None).unwrap()) + .unwrap(); + + assert!(matches!( + tds.is_valid(), + Err(TdsError::InvalidNeighbors { + reason: NeighborValidationError::InteriorFacetNeighborMismatch { .. }, + }) + )); + } + + #[test] + fn is_valid_non_manifold_facet_multiplicity() { + let mut tds: Tds = Tds::empty(); + + let v0 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0])) + .unwrap(); + let v1 = tds + .insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0])) + .unwrap(); + let v2 = tds + .insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0])) + .unwrap(); + let v3 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0])) + .unwrap(); + let v4 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 2.0])) + .unwrap(); + let v5 = tds + .insert_vertex_with_mapping(vertex!([0.0, 0.0, 3.0])) + .unwrap(); + + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v4], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_bypassing_topology_checks_for_test( + Simplex::new(vec![v0, v1, v2, v5], None).unwrap(), + ) + .unwrap(); + + let tri = Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); + + match tri.is_valid() { + Err(InvariantError::Triangulation(TriangulationValidationError::Disconnected { + .. + })) => {} + Err(InvariantError::Triangulation( + TriangulationValidationError::ManifoldFacetMultiplicity { simplex_count, .. }, + )) => assert_eq!(simplex_count, 3), + other => panic!("Expected Disconnected or ManifoldFacetMultiplicity, got {other:?}"), + } + } + + #[test] + fn validation_report_returns_mapping_failures_only() { + 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 tds = + Triangulation::, (), (), 3>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); + + let uuid = tri.tds.vertices().next().unwrap().1.uuid(); + tri.tds.uuid_to_vertex_key.remove(&uuid); + + let report = tri.validation_report().unwrap_err(); + assert!(!report.violations.is_empty()); + assert!(report.violations.iter().all(|v| { + matches!( + v.kind, + InvariantKind::VertexMappings | InvariantKind::SimplexMappings + ) + })); + } + + #[test] + fn validation_report_includes_vertex_and_simplex_validity() { + 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 tds = + Triangulation::, (), (), 3>::build_initial_simplex(&vertices).unwrap(); + let mut tri = + Triangulation::, (), (), 3>::new_with_tds(FastKernel::new(), tds); + + let invalid_vertex: Vertex = Vertex::empty(); + let _ = tri.tds.insert_vertex_with_mapping(invalid_vertex).unwrap(); + + let simplex_key = tri.tds.simplex_keys().next().unwrap(); + let simplex = tri.tds.simplex_mut(simplex_key).unwrap(); + simplex.ensure_neighbors_buffer_mut().truncate(3); + + let report = tri.validation_report().unwrap_err(); + assert!( + report + .violations + .iter() + .any(|v| v.kind == InvariantKind::VertexValidity) + ); + assert!( + report + .violations + .iter() + .any(|v| v.kind == InvariantKind::SimplexValidity) + ); + } + + #[test] + fn validate_after_insertion_required_checks_do_not_run_global_connectedness() { + let tds = build_disconnected_two_triangles_tds_2d(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + tri.set_validation_policy(ValidationPolicy::OnSuspicion); + tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + + assert!(tri.is_valid().is_err()); + tri.validate_after_insertion_with_scope(SuspicionFlags::default(), None) + .unwrap(); + } + + #[test] + fn validate_after_insertion_does_not_skip_pseudomanifold_checks() { + let tds = build_three_triangles_sharing_edge_tds_2d(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + tri.set_validation_policy(ValidationPolicy::OnSuspicion); + tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + + match tri.validate_after_insertion_with_scope(SuspicionFlags::default(), None) { + Err(InvariantError::Triangulation( + TriangulationValidationError::ManifoldFacetMultiplicity { simplex_count, .. }, + )) => assert_eq!(simplex_count, 3), + other => panic!("Expected ManifoldFacetMultiplicity, got {other:?}"), + } + } + + #[test] + fn scoped_validation_catches_touched_over_shared_facet() { + let tds = build_three_triangles_sharing_edge_tds_2d(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + let scope: SimplexKeyBuffer = tri.tds.simplex_keys().take(1).collect(); + + tri.set_validation_policy(ValidationPolicy::OnSuspicion); + tri.set_topology_guarantee(TopologyGuarantee::PLManifold); + + match tri.validate_after_insertion_with_scope(SuspicionFlags::default(), Some(&scope)) { + Err(InvariantError::Triangulation( + TriangulationValidationError::ManifoldFacetMultiplicity { simplex_count, .. }, + )) => assert_eq!(simplex_count, 3), + other => panic!("Expected ManifoldFacetMultiplicity, got {other:?}"), + } + } + + macro_rules! test_scoped_strict_validation_falls_back_to_global_vertex_links { + ($($dim:expr),+ $(,)?) => { + pastey::paste! { + $( + #[test] + fn []() { + let (tds, expected_vertex_key) = build_invalid_vertex_link_tds::<$dim>(); + let mut tri = + Triangulation::, (), (), $dim>::new_with_tds(FastKernel::new(), tds); + let scope: SimplexKeyBuffer = tri.tds.simplex_keys().take(1).collect(); + assert!(!scope.is_empty()); + + tri.validation_policy = ValidationPolicy::OnSuspicion; + tri.topology_guarantee = TopologyGuarantee::PLManifoldStrict; + + match tri.validate_after_insertion_with_scope(SuspicionFlags::default(), Some(&scope)) { + Err(InvariantError::Triangulation( + TriangulationValidationError::RidgeLinkNotManifold { + connected: false, + .. + }, + )) if $dim == 2 => {} + Err(InvariantError::Triangulation( + TriangulationValidationError::VertexLinkNotManifold { vertex_key, .. }, + )) => assert_eq!(vertex_key, expected_vertex_key), + other => panic!("Expected VertexLinkNotManifold, got {other:?}"), + } + } + )+ + } + }; + } + + test_scoped_strict_validation_falls_back_to_global_vertex_links!(2, 3, 4, 5); + + macro_rules! test_insertion_scoped_validation_preserves_full_validity { + ($($dim:expr),+ $(,)?) => { + pastey::paste! { + $( + #[test] + fn []() { + let vertices = unit_simplex_vertices::<$dim>(); + let tds = + Triangulation::, (), (), $dim>::build_initial_simplex(&vertices) + .unwrap(); + let mut tri = + Triangulation::, (), (), $dim>::new_with_tds(FastKernel::new(), tds); + + tri.set_validation_policy(ValidationPolicy::OnSuspicion); + tri.set_topology_guarantee(TopologyGuarantee::PLManifoldStrict); + + let detail = tri + .insert_with_statistics_seeded_indexed_detailed( + unit_simplex_interior_vertex::<$dim>(), + None, + None, + 0, + None, + None, + ) + .unwrap(); + + assert!(!detail.repair_seed_simplices.is_empty()); + tri.validate_after_insertion_with_scope( + SuspicionFlags::default(), + Some(&detail.repair_seed_simplices), + ) + .unwrap(); + tri.is_valid().unwrap(); + } + )+ + } + }; + } + + test_insertion_scoped_validation_preserves_full_validity!(2, 3, 4, 5); + + #[test] + fn validation_report_collects_multiple_violations() { + let (mut tri, _, ck) = build_single_tet(); + + let _ = tri + .tds + .insert_vertex_with_mapping(vertex!([5.0, 5.0, 5.0])) + .unwrap(); + + let simplex = tri.tds.simplex_mut(ck).unwrap(); + simplex.ensure_neighbors_buffer_mut().truncate(2); + + let report = tri.validation_report().unwrap_err(); + assert!( + report.violations.len() >= 2, + "Expected at least 2 violations, got {}", + report.violations.len() + ); + } + + #[test] + fn validate_connectedness_rejects_empty_new_simplices() { + let (tri, _, _) = build_single_tet(); + + let empty: SimplexKeyBuffer = SimplexKeyBuffer::new(); + let err = tri.validate_connectedness(&empty).unwrap_err(); + assert!(matches!(err, InsertionError::TopologyValidation(_))); + } + + #[test] + fn validate_connectedness_passes_for_valid_new_simplices() { + let (tri, _, ck) = build_single_tet(); + + let mut new_simplices = SimplexKeyBuffer::new(); + new_simplices.push(ck); + assert!(tri.validate_connectedness(&new_simplices).is_ok()); + } + + #[test] + fn validate_after_insertion_ok_for_valid_simplex() { + let vertices = [ + 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::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + let suspicion = SuspicionFlags { + repair_loop_entered: true, + ..Default::default() + }; + + assert!( + tri.validate_after_insertion_with_scope(suspicion, None) + .is_ok() + ); + } + + #[test] + fn validate_at_completion_ok_for_pseudomanifold_empty() { + let mut tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + tri.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + + assert!(tri.validate_at_completion().is_ok()); + } + + #[test] + fn validate_at_completion_ok_for_pl_manifold_no_simplices() { + let tri: Triangulation, (), (), 3> = + Triangulation::new_empty(FastKernel::new()); + + assert!(tri.validate_at_completion().is_ok()); + } + + #[test] + fn pl_manifold_insertion_never_commits_invalid_topology_when_validation_policy_is_never() { + let points = generate_random_points_seeded::(25, (-100.0, 100.0), 123).unwrap(); + + let mut dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::empty_with_topology_guarantee(TopologyGuarantee::PLManifold); + + dt.set_validation_policy(ValidationPolicy::Never); + dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); + + for (i, point) in points.into_iter().enumerate() { + let vertex = VertexBuilder::default().point(point).build().unwrap(); + let (outcome, stats) = dt + .insert_with_statistics(vertex) + .unwrap_or_else(|err| panic!("Non-retryable insertion error at i={i}: {err:?}")); + + if dt.number_of_simplices() > 0 + && let Err(err) = dt.as_triangulation().validate() + { + panic!( + "Topology invalid after insertion i={i} (outcome={outcome:?}, attempts={}, used_perturbation={}): {err}", + stats.attempts, + stats.used_perturbation() + ); + } + } + } + + #[test] + fn required_topology_validation_records_telemetry() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + + for guarantee in [ + TopologyGuarantee::Pseudomanifold, + TopologyGuarantee::PLManifold, + ] { + let tds = Triangulation::, (), (), 2>::build_initial_simplex(&vertices) + .unwrap(); + let mut tri = + Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + tri.set_validation_policy(ValidationPolicy::OnSuspicion); + tri.set_topology_guarantee(guarantee); + + let hint = tri.simplices().next().map(|(simplex_key, _)| simplex_key); + let detail = tri + .insert_with_statistics_seeded_indexed_detailed( + vertex!([0.25, 0.25]), + None, + hint, + 0, + None, + None, + ) + .unwrap(); + + assert!(matches!(detail.outcome, InsertionOutcome::Inserted { .. })); + assert!( + detail.telemetry.topology_validation_calls > 0, + "{guarantee:?} insertion should record required topology validation" + ); + assert_eq!( + detail.telemetry.topology_validation_nanos, 0, + "default detailed insertion should not start validation timers" + ); + } + } +} diff --git a/src/core/vertex.rs b/src/core/vertex.rs index 8bc4062a..89530005 100644 --- a/src/core/vertex.rs +++ b/src/core/vertex.rs @@ -20,7 +20,7 @@ //! # Examples //! //! ```rust -//! use delaunay::prelude::triangulation::Vertex; +//! use delaunay::prelude::Vertex; //! use delaunay::vertex; //! //! // Create a simple vertex @@ -66,7 +66,7 @@ use uuid::Uuid; /// /// ```rust /// use delaunay::prelude::tds::UuidValidationError; -/// use delaunay::prelude::triangulation::VertexValidationError; +/// use delaunay::prelude::VertexValidationError; /// /// let err = VertexValidationError::InvalidUuid { /// source: UuidValidationError::NilUuid, @@ -97,7 +97,7 @@ pub enum VertexValidationError { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::VertexBuilderError; +/// use delaunay::prelude::VertexBuilderError; /// /// let err = VertexBuilderError::MissingPoint; /// assert_eq!(err.to_string(), "Missing required field: `point`"); @@ -131,7 +131,7 @@ pub enum VertexBuilderError { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::{Vertex, VertexBuilder}; +/// use delaunay::prelude::{Vertex, VertexBuilder}; /// use delaunay::prelude::geometry::Point; /// use delaunay::prelude::geometry::Coordinate; /// @@ -226,7 +226,7 @@ impl VertexBuilder { /// /// ```rust /// use delaunay::vertex; -/// use delaunay::prelude::triangulation::Vertex; +/// use delaunay::prelude::Vertex; /// /// // Create a vertex without data /// let v1: Vertex = vertex!([1.0, 2.0, 3.0]); @@ -295,7 +295,7 @@ pub use crate::vertex; /// Vertices are typically created using the builder pattern for convenience: /// /// ```rust -/// use delaunay::prelude::triangulation::Vertex; +/// use delaunay::prelude::Vertex; /// use delaunay::vertex; /// /// let vertex: Vertex = vertex!([1.0, 2.0, 3.0], 42); @@ -337,7 +337,7 @@ impl Vertex { /// # Example /// /// ``` - /// use delaunay::prelude::triangulation::Vertex; + /// use delaunay::prelude::Vertex; /// use delaunay::vertex; /// use delaunay::prelude::geometry::Coordinate; /// @@ -358,7 +358,7 @@ impl Vertex { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::*; + /// use delaunay::prelude::*; /// /// let vertices = vec![ /// vertex!([0.0, 0.0]), @@ -382,7 +382,7 @@ impl Vertex { /// /// ``` /// use delaunay::vertex; - /// use delaunay::prelude::triangulation::Vertex; + /// use delaunay::prelude::Vertex; /// /// let v: Vertex = vertex!([1.0, 2.0], 42); /// assert_eq!(v.data(), Some(&42)); @@ -560,7 +560,7 @@ impl Vertex { /// # Examples /// /// ``` - /// use delaunay::prelude::triangulation::Vertex; + /// use delaunay::prelude::Vertex; /// use delaunay::prelude::geometry::Coordinate; /// use approx::assert_relative_eq; /// @@ -597,7 +597,7 @@ impl Vertex { /// /// ```rust /// use delaunay::prelude::geometry::{Coordinate, Point}; - /// use delaunay::prelude::triangulation::Vertex; + /// use delaunay::prelude::Vertex; /// /// let point = Point::new([1.0, 2.0]); /// let vertex = Vertex::::from_point(point); @@ -627,7 +627,7 @@ impl Vertex { /// /// ```rust /// use delaunay::prelude::geometry::{Coordinate, Point}; - /// use delaunay::prelude::triangulation::Vertex; + /// use delaunay::prelude::Vertex; /// /// let point = Point::new([1.0, 2.0]); /// let vertex = Vertex::::from_point_with_data(point, 7); @@ -662,7 +662,7 @@ impl Vertex { /// # Example /// /// ``` - /// use delaunay::prelude::triangulation::Vertex; + /// use delaunay::prelude::Vertex; /// use delaunay::prelude::geometry::Point; /// use delaunay::prelude::geometry::Coordinate; /// let points = [Point::new([1.0, 2.0, 3.0])]; @@ -695,7 +695,7 @@ impl Vertex { /// /// ``` /// use std::collections::HashMap; - /// use delaunay::prelude::triangulation::Vertex; + /// use delaunay::prelude::Vertex; /// use delaunay::vertex; /// /// let v1: Vertex = vertex!([1.0, 2.0]); @@ -759,7 +759,7 @@ impl Vertex { /// # Example /// /// ``` - /// use delaunay::prelude::triangulation::{Vertex, VertexValidationError}; + /// use delaunay::prelude::{Vertex, VertexValidationError}; /// use delaunay::vertex; /// use uuid::Uuid; /// diff --git a/src/triangulation/builder.rs b/src/delaunay/builder.rs similarity index 97% rename from src/triangulation/builder.rs rename to src/delaunay/builder.rs index a0009333..9e7a7688 100644 --- a/src/triangulation/builder.rs +++ b/src/delaunay/builder.rs @@ -34,9 +34,9 @@ //! ## Standard Euclidean construction //! //! ```rust -//! use delaunay::prelude::triangulation::construction::{DelaunayTriangulationBuilder, vertex}; +//! use delaunay::prelude::construction::{DelaunayTriangulationBuilder, vertex}; //! -//! # fn main() -> Result<(), delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError> { +//! # fn main() -> Result<(), delaunay::prelude::construction::DelaunayTriangulationConstructionError> { //! let vertices = vec![ //! vertex!([0.0, 0.0]), //! vertex!([1.0, 0.0]), @@ -54,9 +54,9 @@ //! ## Toroidal construction (Phase 1: canonicalization only) //! //! ```rust -//! use delaunay::prelude::triangulation::construction::{DelaunayTriangulationBuilder, vertex}; +//! use delaunay::prelude::construction::{DelaunayTriangulationBuilder, vertex}; //! -//! # fn main() -> Result<(), delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError> { +//! # fn main() -> Result<(), delaunay::prelude::construction::DelaunayTriangulationConstructionError> { //! // Vertices that fall outside [0, 1)² are wrapped before triangulation. //! let vertices = vec![ //! vertex!([0.2, 0.3]), @@ -81,9 +81,9 @@ //! //! ```rust,no_run //! use delaunay::prelude::geometry::RobustKernel; -//! use delaunay::prelude::triangulation::construction::{DelaunayTriangulationBuilder, vertex}; +//! use delaunay::prelude::construction::{DelaunayTriangulationBuilder, vertex}; //! -//! # fn main() -> Result<(), delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError> { +//! # fn main() -> Result<(), delaunay::prelude::construction::DelaunayTriangulationConstructionError> { //! let vertices = vec![ //! vertex!([0.1, 0.2]), //! vertex!([0.4, 0.7]), @@ -108,10 +108,15 @@ #![forbid(unsafe_code)] +use crate::construction::{ + ConstructionOptions, DelaunayTriangulationConstructionError, InitialSimplexStrategy, + RetryPolicy, +}; use crate::core::algorithms::incremental_insertion::{ DelaunayRepairErrorKind, InsertionError, InsertionErrorSourceKind, }; use crate::core::collections::{FastHashMap, PeriodicOffsetBuffer, Uuid, VertexKeySet}; +use crate::core::construction::TriangulationConstructionError; use crate::core::operations::InsertionOutcome; use crate::core::simplex::{Simplex, SimplexValidationError}; use crate::core::tds::{ @@ -120,12 +125,13 @@ use crate::core::tds::{ TriangulationValidationErrorKind, VertexKey, }; use crate::core::traits::data_type::DataType; -use crate::core::triangulation::{TopologyGuarantee, TriangulationConstructionError}; use crate::core::util::periodic_facet_key_from_lifted_vertices; +use crate::core::validation::TopologyGuarantee; use crate::core::vertex::Vertex; use crate::geometry::kernel::{AdaptiveKernel, Kernel}; use crate::geometry::point::Point; use crate::geometry::traits::coordinate::{Coordinate, CoordinateScalar}; +use crate::repair::DelaunayRepairPolicy; use crate::topology::spaces::toroidal::ToroidalSpace; use crate::topology::traits::global_topology_model::{ GlobalTopologyModel, GlobalTopologyModelError, @@ -133,11 +139,8 @@ use crate::topology::traits::global_topology_model::{ use crate::topology::traits::topological_space::{ GlobalTopology, TopologyKind, ToroidalConstructionMode, }; -use crate::triangulation::delaunay::{ - ConstructionOptions, DelaunayRepairPolicy, DelaunayTriangulation, - DelaunayTriangulationConstructionError, DelaunayTriangulationValidationError, - InitialSimplexStrategy, RetryPolicy, -}; +use crate::triangulation::DelaunayTriangulation; +use crate::validation::DelaunayTriangulationValidationError; use num_traits::ToPrimitive; use rand::SeedableRng; use rand::rngs::StdRng; @@ -354,7 +357,7 @@ pub enum ExplicitTdsErrorKind { /// /// ```rust /// use delaunay::prelude::tds::TdsError; -/// use delaunay::prelude::triangulation::{ExplicitTdsError, ExplicitTdsErrorKind}; +/// use delaunay::prelude::construction::{ExplicitTdsError, ExplicitTdsErrorKind}; /// /// let source = TdsError::InconsistentDataStructure { /// message: "dangling simplex key".to_string(), @@ -468,10 +471,10 @@ pub enum ExplicitInsertionErrorKind { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::{ +/// use delaunay::prelude::construction::{ /// ExplicitInsertionError, ExplicitInsertionErrorKind, /// }; -/// use delaunay::prelude::triangulation::insertion::InsertionError; +/// use delaunay::prelude::insertion::InsertionError; /// /// let source = InsertionError::DuplicateCoordinates { /// coordinates: "[0.0, 0.0]".to_string(), @@ -569,7 +572,7 @@ pub enum ExplicitInvariantErrorKind { /// use delaunay::prelude::tds::{ /// InvariantError, InvariantErrorSummaryDetail, TdsError, TdsErrorKind, /// }; -/// use delaunay::prelude::triangulation::{ +/// use delaunay::prelude::construction::{ /// ExplicitInvariantError, ExplicitInvariantErrorKind, /// }; /// @@ -662,7 +665,7 @@ pub enum ExplicitDelaunayValidationSourceKind { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::{ +/// use delaunay::prelude::construction::{ /// DelaunayTriangulationValidationError, ExplicitDelaunayValidationError, /// ExplicitDelaunayValidationErrorKind, /// }; @@ -745,12 +748,12 @@ impl From for ExplicitDelaunayValidationEr /// explicit-construction path through /// [`DelaunayTriangulationConstructionError::ExplicitConstruction`]. /// -/// [`DelaunayTriangulationConstructionError::ExplicitConstruction`]: crate::triangulation::delaunay::DelaunayTriangulationConstructionError::ExplicitConstruction +/// [`DelaunayTriangulationConstructionError::ExplicitConstruction`]: crate::DelaunayTriangulationConstructionError::ExplicitConstruction /// /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::construction::{ +/// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, /// ExplicitConstructionError, vertex, /// }; @@ -828,7 +831,7 @@ pub enum ExplicitConstructionError { /// only to the Delaunay point-insertion path and are not meaningful for /// explicit simplex construction. /// - /// [`ConstructionOptions`]: crate::triangulation::delaunay::ConstructionOptions + /// [`ConstructionOptions`]: crate::construction::ConstructionOptions #[error( "ConstructionOptions are not applicable to explicit simplex construction \ and must be left at their default values" @@ -913,11 +916,11 @@ pub enum ExplicitConstructionError { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::construction::{ +/// use delaunay::prelude::construction::{ /// ConstructionOptions, DelaunayTriangulationBuilder, TopologyGuarantee, vertex, /// }; /// -/// # fn main() -> Result<(), delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError> { +/// # fn main() -> Result<(), delaunay::prelude::construction::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -987,7 +990,7 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, f64, U, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, Vertex, vertex, /// }; /// @@ -1038,7 +1041,7 @@ impl<'v, U, const D: usize> DelaunayTriangulationBuilder<'v, f64, U, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, /// ExplicitConstructionError, vertex, /// }; @@ -1102,16 +1105,16 @@ impl<'v, T, U, const D: usize> DelaunayTriangulationBuilder<'v, T, U, D> { /// /// ```rust /// use delaunay::prelude::geometry::{Coordinate, Point}; - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, Vertex, VertexBuilder, /// }; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Vertex(#[from] delaunay::prelude::triangulation::construction::VertexBuilderError), + /// # Vertex(#[from] delaunay::prelude::construction::VertexBuilderError), /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::construction::DelaunayTriangulationConstructionError), /// # } /// # fn main() -> Result<(), ExampleError> { /// let vertices: Vec> = vec![ @@ -1155,16 +1158,16 @@ impl<'v, T, U, const D: usize> DelaunayTriangulationBuilder<'v, T, U, D> { /// /// ```rust /// use delaunay::prelude::geometry::{Coordinate, Point}; - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, Vertex, VertexBuilder, /// }; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Vertex(#[from] delaunay::prelude::triangulation::construction::VertexBuilderError), + /// # Vertex(#[from] delaunay::prelude::construction::VertexBuilderError), /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::construction::DelaunayTriangulationConstructionError), /// # } /// # fn main() -> Result<(), ExampleError> { /// // f32 vertices — new() is f64-only, so from_vertices is required here. @@ -1209,11 +1212,11 @@ impl<'v, T, U, const D: usize> DelaunayTriangulationBuilder<'v, T, U, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, vertex, /// }; /// - /// # fn main() -> Result<(), delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError> { + /// # fn main() -> Result<(), delaunay::prelude::construction::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.2, 0.3]), /// vertex!([0.8, 0.1]), @@ -1260,11 +1263,11 @@ impl<'v, T, U, const D: usize> DelaunayTriangulationBuilder<'v, T, U, D> { /// /// ```rust,no_run /// use delaunay::prelude::geometry::RobustKernel; - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, vertex, /// }; /// - /// # fn main() -> Result<(), delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError> { + /// # fn main() -> Result<(), delaunay::prelude::construction::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.1, 0.2]), /// vertex!([0.4, 0.7]), @@ -1299,11 +1302,11 @@ impl<'v, T, U, const D: usize> DelaunayTriangulationBuilder<'v, T, U, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, TopologyGuarantee, vertex, /// }; /// - /// # fn main() -> Result<(), delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError> { + /// # fn main() -> Result<(), delaunay::prelude::construction::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0]), /// vertex!([1.0, 0.0]), @@ -1343,7 +1346,7 @@ impl<'v, T, U, const D: usize> DelaunayTriangulationBuilder<'v, T, U, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, GlobalTopology, ToroidalConstructionMode, vertex, /// }; /// @@ -1376,11 +1379,11 @@ impl<'v, T, U, const D: usize> DelaunayTriangulationBuilder<'v, T, U, D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// ConstructionOptions, DelaunayTriangulationBuilder, InsertionOrderStrategy, vertex, /// }; /// - /// # fn main() -> Result<(), delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError> { + /// # fn main() -> Result<(), delaunay::prelude::construction::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0]), /// vertex!([1.0, 0.0]), @@ -1569,11 +1572,11 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, vertex, /// }; /// - /// # fn main() -> Result<(), delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError> { + /// # fn main() -> Result<(), delaunay::prelude::construction::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -1623,11 +1626,11 @@ where /// /// ```rust /// use delaunay::prelude::geometry::RobustKernel; - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulationBuilder, vertex, /// }; /// - /// # fn main() -> Result<(), delaunay::prelude::triangulation::construction::DelaunayTriangulationConstructionError> { + /// # fn main() -> Result<(), delaunay::prelude::construction::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -3012,6 +3015,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::construction::{DelaunayConstructionFailure, InsertionOrderStrategy}; use crate::core::algorithms::flips::DelaunayRepairError; use crate::core::algorithms::incremental_insertion::{ CavityFillingError, DelaunayRepairFailureContext, HullExtensionReason, NeighborWiringError, @@ -3023,20 +3027,18 @@ mod tests { DelaunayValidationErrorKind, EntityKind, GeometricError, NeighborValidationError, TdsConstructionError, }; - use crate::core::triangulation::TriangulationValidationError; use crate::core::util::uuid::UuidValidationError; + use crate::core::validation::TriangulationValidationError; use crate::core::vertex::VertexBuilder; use crate::core::vertex::VertexValidationError; use crate::geometry::kernel::RobustKernel; + use crate::repair::DelaunayRepairOperation; use crate::topology::traits::global_topology_model::{ EuclideanModel, GlobalTopologyModel, GlobalTopologyModelError, ToroidalModel, }; use crate::topology::traits::topological_space::{ GlobalTopology, TopologyKind, ToroidalConstructionMode, }; - use crate::triangulation::delaunay::{ - DelaunayConstructionFailure, DelaunayRepairOperation, InsertionOrderStrategy, - }; use crate::vertex; use approx::assert_relative_eq; use slotmap::{Key, KeyData}; diff --git a/src/triangulation/delaunay.rs b/src/delaunay/construction.rs similarity index 50% rename from src/triangulation/delaunay.rs rename to src/delaunay/construction.rs index 2debf854..c37d4710 100644 --- a/src/triangulation/delaunay.rs +++ b/src/delaunay/construction.rs @@ -1,226 +1,116 @@ -//! Delaunay triangulation layer with incremental insertion. +//! Batch construction options, errors, statistics, and policy helpers. //! -//! This layer adds Delaunay-specific operations on top of the generic -//! `Triangulation` struct, following CGAL's architecture. +//! This module contains the configuration surface used by +//! [`DelaunayTriangulationBuilder`](crate::builder::DelaunayTriangulationBuilder) +//! and the batch constructors on [`DelaunayTriangulation`](crate::DelaunayTriangulation). +//! Use it when you need deterministic insertion ordering, duplicate handling, +//! initial-simplex selection, retry behavior, or construction telemetry without +//! importing flip editing or validation-only APIs. +//! +//! Most examples should import these items through +//! [`delaunay::prelude::construction`](crate::prelude::construction), which +//! bundles the builder, construction options, construction errors, and the +//! [`vertex!`](crate::vertex) macro. +//! +//! # Examples +//! +//! ```rust +//! use delaunay::prelude::construction::{ +//! ConstructionOptions, DelaunayTriangulationBuilder, +//! DelaunayTriangulationConstructionError, InitialSimplexStrategy, +//! InsertionOrderStrategy, vertex, +//! }; +//! +//! # fn main() -> Result<(), DelaunayTriangulationConstructionError> { +//! let vertices = vec![ +//! vertex!([0.0, 0.0]), +//! vertex!([1.0, 0.0]), +//! vertex!([0.0, 1.0]), +//! ]; +//! +//! let options = ConstructionOptions::default() +//! .with_insertion_order(InsertionOrderStrategy::Input) +//! .with_initial_simplex_strategy(InitialSimplexStrategy::First); +//! +//! let dt = DelaunayTriangulationBuilder::new(&vertices) +//! .construction_options(options) +//! .build::<()>()?; +//! +//! assert_eq!(dt.number_of_vertices(), 3); +//! # Ok(()) +//! # } +//! ``` #![forbid(unsafe_code)] -use crate::core::adjacency::{AdjacencyIndex, AdjacencyIndexBuildError}; +use crate::builder::DelaunayTriangulationBuilder; use crate::core::algorithms::flips::{ - DelaunayRepairError, DelaunayRepairRun, DelaunayRepairStats, FlipError, LocalRepairPhaseTiming, - apply_bistellar_flip_k1_inverse, repair_delaunay_local_single_pass, - repair_delaunay_local_single_pass_timed, repair_delaunay_with_flips_k2_k3, - repair_delaunay_with_flips_k2_k3_run, verify_delaunay_for_triangulation, + DelaunayRepairError, DelaunayRepairStats, FlipError, LocalRepairPhaseTiming, + repair_delaunay_local_single_pass, repair_delaunay_local_single_pass_timed, + repair_delaunay_with_flips_k2_k3, }; use crate::core::algorithms::incremental_insertion::{ - CavityFillingError, DelaunayRepairErrorSummary, DelaunayRepairFailureContext, InsertionError, - TdsConstructionFailure, + CavityFillingError, InsertionError, TdsConstructionFailure, }; use crate::core::algorithms::locate::LocateError; use crate::core::collections::spatial_hash_grid::HashGridIndex; use crate::core::collections::{ - FastHashSet, FastHasher, MAX_PRACTICAL_DIMENSION_SIZE, SecureHashMap, SimplexKeyBuffer, - SmallBuffer, + FastHashSet, FastHasher, MAX_PRACTICAL_DIMENSION_SIZE, SecureHashMap, SmallBuffer, }; -use crate::core::edge::EdgeKey; -use crate::core::facet::{AllFacetsIter, BoundaryFacetsIter}; +use crate::core::construction::TriangulationConstructionError; +use crate::core::insertion::record_duplicate_detection_metrics; use crate::core::operations::{ DelaunayInsertionState, InsertionOutcome, InsertionResult, InsertionStatistics, InsertionTelemetryMode, RepairDecision, TopologicalOperation, }; -use crate::core::simplex::{Simplex, SimplexValidationError}; -use crate::core::tds::{ - InvariantError, InvariantKind, InvariantViolation, NeighborValidationError, SimplexKey, Tds, - TdsConstructionError, TdsError, TriangulationValidationReport, VertexKey, -}; +use crate::core::simplex::SimplexValidationError; +use crate::core::tds::SimplexKey; +use crate::core::tds::{TdsConstructionError, TdsError}; use crate::core::traits::data_type::DataType; -use crate::core::triangulation::{ - TopologyGuarantee, Triangulation, TriangulationConstructionError, TriangulationValidationError, - ValidationPolicy, insertion_error_to_invariant_error, record_duplicate_detection_metrics, -}; +use crate::core::triangulation::Triangulation; use crate::core::util::{ coords_equal_exact, coords_within_epsilon, hilbert_indices_prequantized, hilbert_quantize, - is_delaunay_property_only, stable_hash_u64_slice, + stable_hash_u64_slice, }; +use crate::core::validation::{TopologyGuarantee, ValidationPolicy}; use crate::core::vertex::Vertex; -use crate::geometry::kernel::{AdaptiveKernel, ExactPredicates, Kernel, RobustKernel}; +use crate::diagnostics::{BatchLocalRepairTrigger, ConstructionTelemetry, LocalRepairSample}; +use crate::geometry::kernel::{AdaptiveKernel, Kernel}; use crate::geometry::point::Point; use crate::geometry::traits::coordinate::{Coordinate, CoordinateScalar}; use crate::geometry::util::{safe_coords_to_f64, safe_usize_to_scalar, simplex_volume}; -use crate::topology::manifold::{ManifoldError, validate_ridge_links_for_simplices}; -use crate::topology::traits::topological_space::{GlobalTopology, TopologyKind}; -use crate::triangulation::builder::DelaunayTriangulationBuilder; -use crate::triangulation::diagnostics::{ - BatchLocalRepairTrigger, ConstructionTelemetry, LocalRepairSample, -}; -use crate::triangulation::locality::{ +use crate::locality::{ accumulate_live_simplex_seeds, clear_simplex_seed_set, retain_live_simplex_seeds, }; +use crate::repair::DelaunayRepairPolicy; +use crate::topology::traits::topological_space::GlobalTopology; +use crate::triangulation::DelaunayTriangulation; use core::{cmp::Ordering, fmt}; use num_traits::{NumCast, ToPrimitive, Zero}; use rand::SeedableRng; use rand::rngs::StdRng; use rand::seq::SliceRandom; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::env; -use std::hash::{Hash, Hasher}; -use std::num::NonZeroUsize; -use std::time::{Duration, Instant}; +use std::{ + env, + hash::{Hash, Hasher}, + num::NonZeroUsize, + time::{Duration, Instant}, +}; use thiserror::Error; use uuid::Uuid; +/// Number of deterministic shuffled reconstruction attempts used by the +/// default construction retry policy. const DELAUNAY_SHUFFLE_ATTEMPTS: usize = 6; -const DELAUNAY_SHUFFLE_SEED_SALT: u64 = 0x9E37_79B9_7F4A_7C15; -const INITIAL_SIMPLEX_MAX_VOLUME_CANDIDATE_CAP: usize = 18; - -// Heuristic rebuild attempts must be consistent across build profiles to avoid -// release-only construction failures (see #306). -const HEURISTIC_REBUILD_ATTEMPTS: usize = 6; - -// Per-insertion local-repair flip-budget tunables. -// -// Budget formula: `seed_simplices.len() * (D + 1) * FACTOR` with a minimum of -// `FLOOR`. Two regimes so that D≥4's higher queue demand does not force a -// global budget increase. -// -// The D≥4 constants are sized from the measured `max_queue` distribution on the -// 500-point 4D seeded repro (seed `0xD225B8A07E274AE6`, ball radius 100) -// captured in `docs/archive/issue_204_investigation.md`: -// -// max_queue samples min=91 p50=207 p90=281 p95=312 p99=409 max=416 -// -// `FACTOR = 12` with `FLOOR = 96` yields a typical 300-flip budget (5-simplex seed -// set), covering p50–p90 and brushing p95. The p95–p99 tail is deferred to the -// final completion repair rather than paid for during every cadenced repair. -pub(crate) const LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_GE_4: usize = 12; -pub(crate) const LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_GE_4: usize = 96; -pub(crate) const LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_LT_4: usize = 4; -pub(crate) const LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_LT_4: usize = 16; -const LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_GE_4: usize = 24; -const LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_LT_4: usize = 16; -const RIDGE_LINK_REPAIR_VALIDATION_MESSAGE: &str = "Topology invalid after Delaunay repair"; - -fn ridge_link_repair_validation_error(err: ManifoldError) -> InsertionError { - match TriangulationValidationError::try_from(err) { - Ok(source) => InsertionError::TopologyValidationFailed { - message: RIDGE_LINK_REPAIR_VALIDATION_MESSAGE.to_string(), - source, - }, - Err(source) => InsertionError::TopologyValidation(source), - } -} - -/// Returns true when a repair error represents input geometry or predicate -/// instability that shuffled construction may be able to resolve. -const fn is_geometric_repair_error(repair_err: &DelaunayRepairError) -> bool { - match repair_err { - DelaunayRepairError::NonConvergent { .. } - | DelaunayRepairError::PostconditionFailed { .. } => true, - DelaunayRepairError::VerificationFailed { source, .. } => is_geometric_flip_error(source), - DelaunayRepairError::Flip(source) => is_geometric_flip_error(source), - DelaunayRepairError::OrientationCanonicalizationFailed { .. } - | DelaunayRepairError::InvalidTopology { .. } - | DelaunayRepairError::HeuristicRebuildFailed { .. } => false, - } -} - -/// Returns true for flip errors caused by geometric predicates or degenerate -/// replacement simplices rather than deterministic topology/simplex-key failures. -const fn is_geometric_flip_error(error: &FlipError) -> bool { - matches!( - error, - FlipError::PredicateFailure { .. } - | FlipError::DegenerateSimplex - | FlipError::NegativeOrientation { .. } - | FlipError::SimplexCreation( - SimplexValidationError::DegenerateSimplex - | SimplexValidationError::CoordinateConversion { .. }, - ) - ) -} - -/// Per-insertion local Delaunay repair flip budget. -/// -/// Computes `seeds * (D + 1) * FACTOR` with a minimum of `FLOOR`, using the -/// dimension-aware constants above. -const fn local_repair_flip_budget(seed_simplices_len: usize) -> usize { - let (factor, floor) = if D >= 4 { - ( - LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_GE_4, - LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_GE_4, - ) - } else { - ( - LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_LT_4, - LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_LT_4, - ) - }; - let raw = seed_simplices_len - .saturating_mul(D + 1) - .saturating_mul(factor); - if raw > floor { raw } else { floor } -} - -/// Pending local repair frontier size that triggers an early batch repair. -/// -/// The threshold keeps sparse repair cadences from letting a large seed -/// frontier accumulate. 3D uses a lower threshold because the 3000-point sweep -/// in #341 showed that repair cost rises sharply once the pending frontier -/// grows beyond the small-batch regime. -const fn local_repair_seed_backlog_threshold() -> usize { - let factor = if D >= 4 { - LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_GE_4 - } else { - LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_LT_4 - }; - (D + 1).saturating_mul(factor) -} - -/// Default local-repair cadence for batch construction. -/// -/// Direct incremental insertion keeps [`DelaunayRepairPolicy::default`] at -/// [`DelaunayRepairPolicy::EveryInsertion`]. Batch construction uses the same -/// default because the #341 1000/3000-point proxy sweeps showed every-insertion -/// repair preserved all vertices and was slightly faster than the N=2 cadence. -const fn default_batch_repair_policy() -> DelaunayRepairPolicy { - DelaunayRepairPolicy::EveryInsertion -} - -/// Decides whether batch construction should run local Delaunay repair now. -fn batch_local_repair_trigger( - policy: DelaunayRepairPolicy, - insertion_count: usize, - topology: TopologyGuarantee, - pending_seed_simplices_len: usize, -) -> Option { - if policy == DelaunayRepairPolicy::Never - || pending_seed_simplices_len == 0 - || !TopologicalOperation::FacetFlip.is_admissible_under(topology) - { - return None; - } - - if matches!( - policy.decide(insertion_count, topology, TopologicalOperation::FacetFlip,), - RepairDecision::Proceed - ) { - return Some(BatchLocalRepairTrigger::Cadence); - } - (pending_seed_simplices_len >= local_repair_seed_backlog_threshold::()) - .then_some(BatchLocalRepairTrigger::SeedBacklog) -} +const DELAUNAY_SHUFFLE_SEED_SALT: u64 = 0x9E37_79B9_7F4A_7C15; fn batch_repair_trace_enabled() -> bool { env::var_os("DELAUNAY_BATCH_REPAIR_TRACE").is_some() } -thread_local! { - static HEURISTIC_REBUILD_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; -} - #[cfg(test)] -mod test_hooks { +pub(crate) mod test_hooks { use crate::core::algorithms::flips::{ DelaunayRepairDiagnostics, DelaunayRepairError, RepairQueueOrder, }; @@ -232,11 +122,12 @@ mod test_hooks { static BATCH_LOCAL_REPAIR_CALLS: Cell = const { Cell::new(0) }; } - pub(super) fn force_heuristic_rebuild_enabled() -> bool { + pub fn force_heuristic_rebuild_enabled() -> bool { FORCE_HEURISTIC_REBUILD.with(Cell::get) } - pub(super) fn set_force_heuristic_rebuild(enabled: bool) -> bool { + #[must_use] + pub fn set_force_heuristic_rebuild(enabled: bool) -> bool { FORCE_HEURISTIC_REBUILD.with(|flag| { let prior = flag.get(); flag.set(enabled); @@ -244,15 +135,16 @@ mod test_hooks { }) } - pub(super) fn restore_force_heuristic_rebuild(prior: bool) { + pub fn restore_force_heuristic_rebuild(prior: bool) { FORCE_HEURISTIC_REBUILD.with(|flag| flag.set(prior)); } - pub(super) fn force_repair_nonconvergent_enabled() -> bool { + pub fn force_repair_nonconvergent_enabled() -> bool { FORCE_REPAIR_NONCONVERGENT.with(Cell::get) } - pub(super) fn set_force_repair_nonconvergent(enabled: bool) -> bool { + #[must_use] + pub fn set_force_repair_nonconvergent(enabled: bool) -> bool { FORCE_REPAIR_NONCONVERGENT.with(|flag| { let prior = flag.get(); flag.set(enabled); @@ -260,23 +152,24 @@ mod test_hooks { }) } - pub(super) fn restore_force_repair_nonconvergent(prior: bool) { + pub fn restore_force_repair_nonconvergent(prior: bool) { FORCE_REPAIR_NONCONVERGENT.with(|flag| flag.set(prior)); } - pub(super) fn reset_batch_local_repair_calls() { + pub fn reset_batch_local_repair_calls() { BATCH_LOCAL_REPAIR_CALLS.with(|calls| calls.set(0)); } - pub(super) fn batch_local_repair_calls() -> usize { + pub fn batch_local_repair_calls() -> usize { BATCH_LOCAL_REPAIR_CALLS.with(Cell::get) } - pub(super) fn record_batch_local_repair_call() { + pub fn record_batch_local_repair_call() { BATCH_LOCAL_REPAIR_CALLS.with(|calls| calls.set(calls.get().saturating_add(1))); } - pub(super) fn synthetic_nonconvergent_error() -> DelaunayRepairError { + #[must_use] + pub fn synthetic_nonconvergent_error() -> DelaunayRepairError { DelaunayRepairError::NonConvergent { max_flips: 0, diagnostics: Box::new(DelaunayRepairDiagnostics { @@ -295,67 +188,17 @@ mod test_hooks { } } -struct HeuristicRebuildRecursionGuard { - prior_depth: usize, -} - -impl HeuristicRebuildRecursionGuard { - /// Tracks nested heuristic rebuilds so fallback construction cannot recurse - /// indefinitely through repair hooks. - fn enter() -> Self { - let prior_depth = HEURISTIC_REBUILD_DEPTH.with(|depth| { - let prior = depth.get(); - depth.set(prior.saturating_add(1)); - prior - }); - Self { prior_depth } - } -} - -impl Drop for HeuristicRebuildRecursionGuard { - fn drop(&mut self) { - HEURISTIC_REBUILD_DEPTH.with(|depth| depth.set(self.prior_depth)); - } -} - /// Errors that can occur during Delaunay triangulation construction. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::construction::{ -/// DelaunayTriangulation, DelaunayTriangulationConstructionError, vertex, -/// }; -/// -/// 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 result: Result<_, DelaunayTriangulationConstructionError> = -/// DelaunayTriangulation::new(&vertices); -/// assert!(result.is_ok()); -/// ``` #[derive(Clone, Debug, Error, PartialEq, Eq)] #[non_exhaustive] pub enum DelaunayTriangulationConstructionError { /// Lower-layer construction failure summarized for Delaunay construction. - /// - /// See [`DelaunayConstructionFailure`] for the pattern-matchable payload and - /// for which lower-layer sources are preserved versus summarized. #[error(transparent)] Triangulation(DelaunayConstructionFailure), /// Input validation error from explicit combinatorial construction. - /// - /// Returned by [`DelaunayTriangulationBuilder::from_vertices_and_simplices`](crate::triangulation::builder::DelaunayTriangulationBuilder::from_vertices_and_simplices) - /// when the caller-provided vertices/simplices fail validation (wrong arity, - /// out-of-bounds indices, etc.). TDS assembly errors flow through the - /// [`Triangulation`](Self::Triangulation) variant instead. #[error(transparent)] - ExplicitConstruction(#[from] crate::triangulation::builder::ExplicitConstructionError), + ExplicitConstruction(#[from] crate::builder::ExplicitConstructionError), } impl From for DelaunayTriangulationConstructionError { @@ -365,10 +208,6 @@ impl From for DelaunayTriangulationConstructionE } /// Construction phase that invoked flip-based Delaunay repair. -/// -/// Batch construction can run local repair at the configured cadence or earlier -/// when the pending seed frontier grows too large. Both cases are reported as -/// [`Self::BatchLocal`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum DelaunayConstructionRepairPhase { @@ -384,49 +223,13 @@ pub enum DelaunayConstructionRepairPhase { impl fmt::Display for DelaunayConstructionRepairPhase { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::BatchLocal { index } => { - write!(f, "batch local repair at input index {index}") - } + Self::BatchLocal { index } => write!(f, "batch local repair at input index {index}"), Self::Completion => f.write_str("completion repair"), } } } /// Pattern-matchable summary of a lower-layer construction failure. -/// -/// This is the payload for -/// [`DelaunayTriangulationConstructionError::Triangulation`]. It keeps common -/// construction failure classes orthogonal for callers that need to distinguish -/// input-size errors, duplicate UUIDs, predicate/location failures, topology -/// validation failures, and internal invariant breaches. -/// -/// Typed lower-layer sources are preserved where the public Delaunay error can -/// expose them without coupling unrelated layers: for example [`Tds`](Self::Tds), -/// [`InsufficientVertices`](Self::InsufficientVertices), and -/// [`InsertionCavityFilling`](Self::InsertionCavityFilling) carry structured sources. -/// Other insertion, repair, and validation paths are summarized as strings -/// because they may aggregate multiple retry attempts or cross layer boundaries. -/// Those variants remain stable buckets for matching, but callers should treat -/// their `message` fields as diagnostics rather than machine-readable data. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::construction::{ -/// DelaunayConstructionFailure, DelaunayTriangulation, -/// DelaunayTriangulationConstructionError, vertex, -/// }; -/// -/// let vertices = vec![vertex!([0.0, 0.0, 0.0])]; -/// let err = DelaunayTriangulation::new(&vertices).unwrap_err(); -/// -/// match err { -/// DelaunayTriangulationConstructionError::Triangulation( -/// DelaunayConstructionFailure::InsufficientVertices { dimension, .. }, -/// ) => assert_eq!(dimension, 3), -/// other => panic!("unexpected construction error: {other:?}"), -/// } -/// ``` #[derive(Clone, Debug, Error, PartialEq, Eq)] #[non_exhaustive] pub enum DelaunayConstructionFailure { @@ -484,7 +287,7 @@ pub enum DelaunayConstructionFailure { phase: DelaunayConstructionRepairPhase, /// Underlying typed repair failure. #[source] - source: Box, + source: Box, }, /// Duplicate coordinates were detected. @@ -542,9 +345,6 @@ pub enum DelaunayConstructionFailure { }, /// Final topology validation failed after construction. - /// - /// Mirrors insertion-time topology validation for post-build checks that - /// run after incremental insertion has completed. #[error("final topology validation failed after construction: {message}: {source}")] FinalTopologyValidation { /// Validation failure detail. @@ -616,354 +416,99 @@ impl From for DelaunayConstructionFailure { } } -/// Mutating Delaunay operation that can invoke flip-based repair internally. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::repair::DelaunayRepairOperation; -/// -/// assert_eq!(DelaunayRepairOperation::VertexRemoval.to_string(), "vertex removal"); -/// ``` -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum DelaunayRepairOperation { - /// Repair after removing a vertex. - VertexRemoval, -} - -impl fmt::Display for DelaunayRepairOperation { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::VertexRemoval => f.write_str("vertex removal"), - } - } -} - -/// Errors that can occur during Delaunay triangulation validation and repair. -/// -/// The first three variants are returned by [`DelaunayTriangulation::validate`] -/// (validation Levels 1–4): -/// - [`Tds`](Self::Tds) — element or TDS structural errors (Levels 1–2). -/// - [`Triangulation`](Self::Triangulation) — topology errors (Level 3). -/// - [`VerificationFailed`](Self::VerificationFailed) — Delaunay property violation (Level 4). -/// -/// [`DelaunayTriangulation::is_valid`] returns only the Level 4 -/// [`VerificationFailed`](Self::VerificationFailed) variant. -/// -/// The repair-failure variants are **not** returned by `validate()` or -/// `is_valid()`. They are produced by mutating operations that invoke -/// flip-based repair internally (e.g. [`DelaunayTriangulation::remove_vertex`]). -/// -/// When manually forwarding lower-layer validation errors, prefer -/// `DelaunayTriangulationValidationError::from(tds_error)` or `.into()` for -/// [`TdsError`] and [`TriangulationValidationError`]. The enum stores those -/// sources behind `Box` to keep `Result<_, DelaunayTriangulationValidationError>` -/// compact while preserving typed error inspection. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -/// use delaunay::prelude::triangulation::validation::DelaunayTriangulationValidationError; -/// -/// 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::new(&vertices).unwrap(); -/// -/// let result: Result<(), DelaunayTriangulationValidationError> = dt.validate(); -/// assert!(result.is_ok()); -/// ``` -#[derive(Clone, Debug, Error, PartialEq, Eq)] -#[non_exhaustive] -pub enum DelaunayTriangulationValidationError { - /// Lower-layer element or TDS structural validation error (Levels 1–2). - #[error(transparent)] - Tds(Box), - - /// Lower-layer topology validation error (Level 3). - #[error(transparent)] - Triangulation(Box), - - /// Flip-based Delaunay verification detected a violation. - /// - /// This is returned by [`DelaunayTriangulation::is_valid`] when the fast - /// O(simplices) flip-predicate scan finds a Delaunay violation. The error is - /// a Level 4 (Delaunay property) issue, not a Level 1–2 structural problem. - #[error("Delaunay verification failed: {message}")] - VerificationFailed { - /// Description of the verification failure. - message: String, - }, - - /// Flip-based Delaunay repair failed with string-only context. - /// - /// This variant is retained for compatibility with existing callers. New - /// mutating operations that can preserve the repair source should prefer - /// [`RepairOperationFailed`](Self::RepairOperationFailed). - /// - /// **Not** returned by `validate()` or `is_valid()` — those use - /// [`VerificationFailed`](Self::VerificationFailed) for passive checks. - #[error("Delaunay repair failed: {message}")] - RepairFailed { - /// Description of the repair failure. - message: String, - }, - - /// Flip-based Delaunay repair failed during a specific mutating operation. - /// - /// This preserves the underlying [`DelaunayRepairError`] so callers can - /// inspect budget exhaustion, topology errors, predicate failures, and other - /// repair causes without parsing display text. Operations that report this - /// variant are responsible for documenting whether failure is transactional; - /// [`remove_vertex`](DelaunayTriangulation::remove_vertex) restores the - /// pre-removal triangulation when post-removal repair fails. - /// - /// **Not** returned by `validate()` or `is_valid()` — those use - /// [`VerificationFailed`](Self::VerificationFailed) for passive checks. - #[error("Delaunay repair failed during {operation}: {source}")] - RepairOperationFailed { - /// Mutating operation that invoked repair. - operation: DelaunayRepairOperation, - /// Underlying flip-repair failure. - #[source] - source: Box, - }, -} - -impl From for DelaunayTriangulationValidationError { - fn from(source: TdsError) -> Self { - Self::Tds(Box::new(source)) +/// Returns true when a repair error represents input geometry or predicate +/// instability that shuffled construction may be able to resolve. +const fn is_geometric_repair_error(repair_err: &DelaunayRepairError) -> bool { + match repair_err { + DelaunayRepairError::NonConvergent { .. } + | DelaunayRepairError::PostconditionFailed { .. } => true, + DelaunayRepairError::VerificationFailed { source, .. } => is_geometric_flip_error(source), + DelaunayRepairError::Flip(source) => is_geometric_flip_error(source), + DelaunayRepairError::OrientationCanonicalizationFailed { .. } + | DelaunayRepairError::InvalidTopology { .. } + | DelaunayRepairError::HeuristicRebuildFailed { .. } => false, } } -impl From for DelaunayTriangulationValidationError { - fn from(source: TriangulationValidationError) -> Self { - Self::Triangulation(Box::new(source)) - } +/// Returns true for flip errors caused by geometric predicates or degenerate +/// replacement simplices rather than deterministic topology/simplex-key failures. +const fn is_geometric_flip_error(error: &FlipError) -> bool { + matches!( + error, + FlipError::PredicateFailure { .. } + | FlipError::DegenerateSimplex + | FlipError::NegativeOrientation { .. } + | FlipError::SimplexCreation( + SimplexValidationError::DegenerateSimplex + | SimplexValidationError::CoordinateConversion { .. }, + ) + ) } -// ============================================================================= -// BATCH CONSTRUCTION OPTIONS -// ============================================================================= - /// Strategy used to order input vertices before batch construction. -/// -/// The default is [`InsertionOrderStrategy::Hilbert`], which improves spatial locality during -/// bulk insertion and provides unconditional quantized dedup. -/// -/// If you need to preserve the caller-provided order (for example to control the initial simplex -/// vertices), use [`InsertionOrderStrategy::Input`]. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::construction::{ -/// ConstructionOptions, DelaunayTriangulation, InsertionOrderStrategy, vertex, -/// }; -/// -/// 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 options = ConstructionOptions::default() -/// .with_insertion_order(InsertionOrderStrategy::Input); -/// let dt = DelaunayTriangulation::new_with_options(&vertices, options).unwrap(); -/// assert_eq!(dt.number_of_vertices(), 4); -/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[non_exhaustive] pub enum InsertionOrderStrategy { - /// Preserve the caller-provided input order (no reordering). + /// Preserve the caller-provided input order. Input, - /// Sort vertices by Hilbert curve (quantized, normalized coordinates). - /// - /// This ordering can improve spatial locality during bulk insertion, reducing point location - /// cost. - /// - /// Ties are broken lexicographically by coordinates, then by original input index. + /// Sort vertices by Hilbert curve with deterministic tie-breaking. #[default] Hilbert, } -/// Policy controlling optional preprocessing to remove duplicate or near-duplicate vertices -/// before batch construction. -/// -/// This is a **performance-tuning** knob, not a correctness requirement. The -/// triangulation engine applies two built-in safety layers: -/// -/// 1. **Hilbert quantized dedup** — when -/// [`InsertionOrderStrategy::Hilbert`] is active (the default), vertices -/// that map to the same quantized grid cell are removed in a single O(n) -/// sweep during sorting (zero extra cost since the quantized coordinates -/// are already computed). This runs regardless of `DedupPolicy`, but only -/// when the Hilbert insertion order is selected. -/// 2. **Per-insertion duplicate check** *(unconditional)* — every `insert` -/// call checks the incoming vertex against existing vertices -/// (squared-distance tolerance 1e-10). Duplicates are skipped without -/// modifying the triangulation. -/// -/// Use `DedupPolicy::Exact` or `DedupPolicy::Epsilon` when your input is -/// known to contain many duplicates and you want to avoid the per-vertex -/// insertion overhead for each one. -/// -/// The default is [`DedupPolicy::Off`]. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::construction::{ -/// ConstructionOptions, DedupPolicy, DelaunayTriangulation, vertex, -/// }; -/// -/// 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 options = ConstructionOptions::default().with_dedup_policy(DedupPolicy::Exact); -/// let dt = DelaunayTriangulation::new_with_options(&vertices, options).unwrap(); -/// assert_eq!(dt.number_of_vertices(), 4); -/// ``` +/// Policy controlling optional preprocessing to remove duplicate vertices. #[derive(Debug, Clone, Copy, PartialEq, Default)] #[non_exhaustive] pub enum DedupPolicy { - /// Do not apply explicit preprocessing dedup (rely on the built-in - /// Hilbert quantized dedup and per-insertion duplicate checks). + /// Do not apply explicit preprocessing dedup. #[default] Off, - /// Remove exact coordinate duplicates before construction (NaN-aware, +0.0 == -0.0). - /// - /// This is a performance optimisation for inputs with many exact duplicates; - /// it avoids paying per-vertex insertion cost for each duplicate. + /// Remove exact coordinate duplicates before construction. Exact, - /// Remove near-duplicates within the given Euclidean tolerance before construction. - /// - /// The tolerance is expressed as an `f64` and is converted to the triangulation's scalar type - /// at runtime. Invalid (negative / non-finite) tolerances are rejected. + /// Remove near-duplicates within the given Euclidean tolerance. Epsilon { - /// Non-negative Euclidean tolerance used when considering two vertices identical. + /// Non-negative Euclidean tolerance. tolerance: f64, }, } -/// Strategy controlling how the initial D+1 simplex vertices are selected during batch construction. -/// -/// The default ([`MaxVolume`](Self::MaxVolume)) searches a bounded pool of real extreme vertices -/// for the largest nondegenerate simplex before construction. The -/// [`Balanced`](Self::Balanced) strategy chooses a spread-out simplex using a deterministic -/// farthest-point heuristic. The [`First`](Self::First) strategy preserves legacy behavior by -/// taking the first D+1 vertices after preprocessing and insertion-ordering. -/// -/// These strategies only change construction order. They never introduce synthetic vertices, -/// relax topology checks, or bypass final Delaunay validation. If a strategy that reorders -/// vertices cannot select a usable initial simplex, preprocessing falls back to the existing vertex -/// order and the normal construction error path decides whether the input is valid. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::construction::{ -/// ConstructionOptions, InitialSimplexStrategy, -/// }; -/// -/// let options = ConstructionOptions::default(); -/// -/// assert_eq!( -/// options.initial_simplex_strategy(), -/// InitialSimplexStrategy::MaxVolume, -/// ); -/// ``` +/// Strategy controlling how the initial D+1 simplex vertices are selected. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[non_exhaustive] pub enum InitialSimplexStrategy { /// Use the first D+1 vertices after preprocessing. - /// - /// This preserves the legacy construction order and is useful when callers need exact - /// compatibility with an explicitly supplied insertion sequence. First, /// Choose a better-conditioned simplex using a deterministic farthest-point heuristic. Balanced, /// Choose the largest-volume simplex from a bounded real-vertex candidate pool. - /// - /// This is the default because a larger real starting simplex can reduce early convex-hull - /// insertions and their associated local repair work, especially for large 3D point clouds. - /// Candidate scoring is a deterministic preprocessing heuristic; correctness still comes from - /// the ordinary construction, repair, and validation pipeline. #[default] MaxVolume, } -/// Policy controlling deterministic "retry with alternative insertion orders" during batch -/// construction. -/// -/// When enabled, the constructor deterministically retries construction with alternative insertion -/// orders (shuffles) when the initial attempt fails (e.g. flip-repair cycling on co-spherical -/// configurations). The default is [`Shuffled`](Self::Shuffled) with 6 attempts, which is -/// essential for robust 3D+ construction. Use [`Disabled`](Self::Disabled) to opt out. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::construction::{ -/// ConstructionOptions, DelaunayTriangulation, RetryPolicy, vertex, -/// }; -/// -/// 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 options = ConstructionOptions::default().with_retry_policy(RetryPolicy::Disabled); -/// let dt = DelaunayTriangulation::new_with_options(&vertices, options).unwrap(); -/// assert_eq!(dt.number_of_vertices(), 4); -/// ``` +/// Policy controlling deterministic retries with alternative insertion orders. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum RetryPolicy { /// Do not attempt shuffled reconstruction retries. Disabled, - /// Retry construction with a small number of deterministic shuffles if the final Delaunay - /// property check fails. + /// Retry construction with deterministic shuffles. Shuffled { - /// Number of shuffled reconstruction attempts (excluding the original-order attempt). + /// Number of shuffled reconstruction attempts. attempts: NonZeroUsize, - /// Optional base seed. If `None`, a deterministic seed is derived from the vertex set. + /// Optional base seed. base_seed: Option, }, - /// Retry construction with a small number of deterministic shuffles if the - /// final Delaunay property check fails, but only in debug/test builds. - /// - /// In release builds, this is treated as [`RetryPolicy::Disabled`]. - /// - /// Note: [`RetryPolicy::default()`] now returns [`Shuffled`](Self::Shuffled) - /// in all build modes, so this variant is only useful when you explicitly - /// want retries restricted to debug/test builds. + /// Retry with deterministic shuffles only in debug/test builds. DebugOnlyShuffled { - /// Number of shuffled reconstruction attempts (excluding the original-order attempt). + /// Number of shuffled reconstruction attempts. attempts: NonZeroUsize, - /// Optional base seed. If `None`, a deterministic seed is derived from the vertex set. + /// Optional base seed. base_seed: Option, }, } impl Default for RetryPolicy { fn default() -> Self { - // Shuffled retries are essential for correctness: certain Hilbert-sorted - // insertion orders produce co-spherical configurations that cause flip-repair - // cycling. Retrying with a different order avoids the problematic sequence. - // Previously disabled in release builds, which caused #306. Self::Shuffled { attempts: NonZeroUsize::new(DELAUNAY_SHUFFLE_ATTEMPTS) .expect("DELAUNAY_SHUFFLE_ATTEMPTS must be non-zero"), @@ -972,46 +517,21 @@ impl Default for RetryPolicy { } } +/// Default local-repair cadence for batch construction. +const fn default_batch_repair_policy() -> DelaunayRepairPolicy { + DelaunayRepairPolicy::EveryInsertion +} + /// Options controlling batch construction behavior. -/// -/// Higher-level constructors delegate to the options-based constructor using -/// [`ConstructionOptions::default`]. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::construction::{ -/// ConstructionOptions, DedupPolicy, DelaunayRepairPolicy, InsertionOrderStrategy, RetryPolicy, -/// }; -/// use std::num::NonZeroUsize; -/// -/// let options = ConstructionOptions::default() -/// .with_insertion_order(InsertionOrderStrategy::Hilbert) -/// .with_dedup_policy(DedupPolicy::Off) -/// .with_batch_repair_policy(DelaunayRepairPolicy::EveryN( -/// NonZeroUsize::new(4).unwrap(), -/// )) -/// .with_retry_policy(RetryPolicy::Disabled); -/// -/// assert_eq!(options.insertion_order(), InsertionOrderStrategy::Hilbert); -/// assert_eq!( -/// options.batch_repair_policy(), -/// DelaunayRepairPolicy::EveryN(NonZeroUsize::new(4).unwrap()), -/// ); -/// ``` #[derive(Debug, Clone, Copy, PartialEq)] #[non_exhaustive] pub struct ConstructionOptions { - insertion_order: InsertionOrderStrategy, - dedup_policy: DedupPolicy, - initial_simplex: InitialSimplexStrategy, - retry_policy: RetryPolicy, - batch_repair_policy: DelaunayRepairPolicy, - /// When `true` (default), final bulk repair can fall back to a global - /// `repair_delaunay_with_flips_k2_k3` pass before acceptance when the - /// seeded completion pass cycles. Set to `false` for constructions where - /// global repair could disrupt the triangulation topology (e.g. periodic - /// image-point builds). + pub(crate) insertion_order: InsertionOrderStrategy, + pub(crate) dedup_policy: DedupPolicy, + pub(crate) initial_simplex: InitialSimplexStrategy, + pub(crate) retry_policy: RetryPolicy, + pub(crate) batch_repair_policy: DelaunayRepairPolicy, + /// Whether final bulk repair can fall back to a global repair pass. pub(crate) use_global_repair_fallback: bool, } @@ -1040,6 +560,7 @@ impl ConstructionOptions { pub const fn dedup_policy(&self) -> DedupPolicy { self.dedup_policy } + /// Returns the strategy used to select the initial simplex. #[must_use] pub const fn initial_simplex_strategy(&self) -> InitialSimplexStrategy { @@ -1053,23 +574,6 @@ impl ConstructionOptions { } /// Returns the automatic local Delaunay repair policy used during batch construction. - /// - /// [`DelaunayRepairPolicy::EveryN`] controls the scheduled cadence. Batch - /// construction may also run an earlier local repair when the accumulated - /// seed frontier grows large; [`DelaunayRepairPolicy::Never`] disables both. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// ConstructionOptions, DelaunayRepairPolicy, - /// }; - /// - /// assert_eq!( - /// ConstructionOptions::default().batch_repair_policy(), - /// DelaunayRepairPolicy::EveryInsertion, - /// ); - /// ``` #[must_use] pub const fn batch_repair_policy(&self) -> DelaunayRepairPolicy { self.batch_repair_policy @@ -1088,29 +592,8 @@ impl ConstructionOptions { self.dedup_policy = dedup_policy; self } + /// Sets the initial simplex selection strategy. - /// - /// Use this as a construction-ordering performance knob. The strategy selects real input - /// vertices for the starting simplex and does not change repair policy, topology guarantees, - /// or final validation. Call this with [`InitialSimplexStrategy::Balanced`] or - /// [`InitialSimplexStrategy::First`] to opt out of the default - /// [`InitialSimplexStrategy::MaxVolume`] heuristic. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// ConstructionOptions, InitialSimplexStrategy, - /// }; - /// - /// let options = ConstructionOptions::default() - /// .with_initial_simplex_strategy(InitialSimplexStrategy::Balanced); - /// - /// assert_eq!( - /// options.initial_simplex_strategy(), - /// InitialSimplexStrategy::Balanced, - /// ); - /// ``` #[must_use] pub const fn with_initial_simplex_strategy( mut self, @@ -1128,28 +611,6 @@ impl ConstructionOptions { } /// Sets the automatic local Delaunay repair policy used during batch construction. - /// - /// [`DelaunayRepairPolicy::EveryN`] controls the scheduled cadence. Batch - /// construction may also run an earlier local repair when the accumulated - /// seed frontier grows large; [`DelaunayRepairPolicy::Never`] disables both. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// ConstructionOptions, DelaunayRepairPolicy, - /// }; - /// use std::num::NonZeroUsize; - /// - /// let repair_every = NonZeroUsize::new(2).expect("literal 2 is nonzero"); - /// let options = ConstructionOptions::default() - /// .with_batch_repair_policy(DelaunayRepairPolicy::EveryN(repair_every)); - /// - /// assert_eq!( - /// options.batch_repair_policy(), - /// DelaunayRepairPolicy::EveryN(repair_every), - /// ); - /// ``` #[must_use] pub const fn with_batch_repair_policy( mut self, @@ -1168,46 +629,32 @@ impl ConstructionOptions { } /// Aggregate statistics collected during batch construction. -/// -/// This summarizes the per-vertex [`InsertionStatistics`] generated by the incremental insertion -/// engine during bulk construction (including vertices that are skipped via transactional rollback). #[derive(Debug, Default, Clone)] #[non_exhaustive] pub struct ConstructionStatistics { - /// Number of vertices successfully inserted (includes the initial D+1 simplex vertices). + /// Number of vertices successfully inserted. pub inserted: usize, /// Number of vertices skipped due to duplicate coordinates. pub skipped_duplicate: usize, - /// Number of vertices skipped due to geometric degeneracy after exhausting retries. + /// Number of vertices skipped due to geometric degeneracy. pub skipped_degeneracy: usize, - /// Total number of insertion attempts across all vertices. pub total_attempts: usize, /// Maximum attempts for any single vertex. pub max_attempts: usize, - /// Histogram of attempts: `attempts_histogram[k]` = number of vertices that took `k` attempts. + /// Histogram of attempts. pub attempts_histogram: Vec, - - /// Number of vertices that required perturbation (attempts > 1). + /// Number of vertices that required perturbation. pub used_perturbation: usize, - - /// Total number of simplices removed during insertion safety-net / repair bookkeeping. + /// Total number of simplices removed during insertion repair bookkeeping. pub simplices_removed_total: usize, - /// Maximum number of simplices removed during repair for any single insertion. + /// Maximum number of simplices removed during repair for one insertion. pub simplices_removed_max: usize, - /// Aggregate batch-construction telemetry. pub telemetry: ConstructionTelemetry, - /// Slowest transactional insertions observed during batch construction. - /// - /// This is intended for diagnosing scaling pathologies and is capped - /// (currently the top 8 by insertion wall time). pub slow_insertions: Vec, - - /// A small set of representative skipped vertices recorded during batch construction. - /// - /// This is intended for debugging/reproduction and is capped (currently the first 8 skips). + /// Representative skipped vertices recorded during batch construction. pub skip_samples: Vec, } @@ -1215,23 +662,17 @@ pub struct ConstructionStatistics { #[derive(Debug, Clone)] #[non_exhaustive] pub struct ConstructionSkipSample { - /// Index in the construction insertion order (after preprocessing and ordering). + /// Index in the construction insertion order. pub index: usize, /// UUID of the skipped vertex. pub uuid: Uuid, - /// Coordinates of the skipped vertex, converted to `f64` for logging/debugging. - /// - /// Empty when [`coords_available`](Self::coords_available) is `false`. + /// Coordinates converted to `f64` for logging/debugging. pub coords: Vec, - /// Whether [`coords`](Self::coords) contains a successfully converted coordinate vector. - /// - /// `false` means at least one coordinate could not be represented as `f64`; - /// callers should omit coordinates rather than treating an empty vector as - /// real geometry. + /// Whether [`coords`](Self::coords) contains converted coordinates. pub coords_available: bool, /// Number of insertion attempts for this vertex. pub attempts: usize, - /// Human-readable error message describing why the vertex was skipped. + /// Human-readable error message. pub error: String, } @@ -1239,7 +680,7 @@ pub struct ConstructionSkipSample { #[derive(Debug, Clone)] #[non_exhaustive] pub struct ConstructionSlowInsertionSample { - /// Index in the construction insertion order (after preprocessing and ordering). + /// Index in the construction insertion order. pub index: usize, /// UUID of the inserted or skipped vertex. pub uuid: Uuid, @@ -1247,7 +688,7 @@ pub struct ConstructionSlowInsertionSample { pub attempts: usize, /// Final insertion result for this vertex. pub result: InsertionResult, - /// Wall-clock nanoseconds spent in the transactional insertion call. + /// Wall-clock nanoseconds spent in insertion. pub elapsed_nanos: u64, /// Simplex count immediately after the insertion attempt. pub simplices_after: usize, @@ -1255,9 +696,9 @@ pub struct ConstructionSlowInsertionSample { pub locate_calls: usize, /// Facet-walk steps performed by this insertion. pub locate_walk_steps_total: usize, - /// Local conflict-region calls performed by this insertion. + /// Conflict-region calls performed by this insertion. pub conflict_region_calls: usize, - /// Local conflict-region simplices observed by this insertion. + /// Conflict-region simplices observed by this insertion. pub conflict_region_simplices_total: usize, /// Cavity insertion calls performed by this insertion. pub cavity_insertion_calls: usize, @@ -1269,14 +710,8 @@ pub struct ConstructionSlowInsertionSample { pub topology_validation_calls: usize, } -/// Construction error that also carries aggregate statistics collected up to the failure point. -/// -/// Returned by statistics constructors such as -/// [`DelaunayTriangulation::new_with_construction_statistics`]. The -/// [`error`](Self::error) field carries the normal construction failure while -/// [`statistics`](Self::statistics) captures the insertion attempts completed -/// before that failure. -#[derive(Debug, Clone, thiserror::Error)] +/// Construction error that also carries aggregate statistics collected up to failure. +#[derive(Debug, Clone, Error)] #[error("{error}")] #[non_exhaustive] #[must_use] @@ -1284,13 +719,12 @@ pub struct DelaunayTriangulationConstructionErrorWithStatistics { /// Underlying construction error. #[source] pub error: DelaunayTriangulationConstructionError, - /// Aggregate construction statistics collected before the error occurred. + /// Aggregate construction statistics collected before the error. pub statistics: ConstructionStatistics, } impl ConstructionStatistics { - /// Aggregates attempt counters shared by inserted, skipped, and duplicate - /// insertion outcomes. + /// Aggregates attempt counters shared by inserted, skipped, and duplicate outcomes. #[inline] fn record_common(&mut self, stats: &InsertionStatistics) { self.total_attempts = self.total_attempts.saturating_add(stats.attempts); @@ -1317,7 +751,7 @@ impl ConstructionStatistics { const MAX_SKIP_SAMPLES: usize = 8; const MAX_SLOW_INSERTION_SAMPLES: usize = 8; - /// Record a single insertion attempt (inserted or skipped). + /// Record a single insertion attempt. pub fn record_insertion(&mut self, stats: &InsertionStatistics) { if stats.skipped_duplicate() { self.skipped_duplicate = self.skipped_duplicate.saturating_add(1); @@ -1338,35 +772,6 @@ impl ConstructionStatistics { } /// Record a slow insertion sample, preserving the top samples by elapsed time. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// ConstructionStatistics, DelaunayTriangulation, vertex, - /// }; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// vertex!([0.25, 0.25]), - /// ]; - /// let (_, stats) = - /// DelaunayTriangulation::<_, (), (), 2>::new_with_construction_statistics(&vertices) - /// .unwrap(); - /// let sample = stats - /// .slow_insertions - /// .first() - /// .cloned() - /// .expect("one non-simplex vertex produces a slow-insertion sample"); - /// - /// let mut summary = ConstructionStatistics::default(); - /// summary.record_slow_insertion_sample(sample.clone()); - /// - /// assert_eq!(summary.slow_insertions.len(), 1); - /// assert_eq!(summary.slow_insertions[0].index, sample.index); - /// ``` pub fn record_slow_insertion_sample(&mut self, sample: ConstructionSlowInsertionSample) { let insert_at = self .slow_insertions @@ -1383,7 +788,7 @@ impl ConstructionStatistics { } /// Merges attempt-level statistics from another construction pass. - fn merge_from(&mut self, other: &Self) { + pub(crate) fn merge_from(&mut self, other: &Self) { self.inserted = self.inserted.saturating_add(other.inserted); self.skipped_duplicate = self .skipped_duplicate @@ -1430,12 +835,97 @@ impl ConstructionStatistics { } } +// Per-insertion local-repair flip-budget tunables. +// +// Budget formula: `seed_simplices.len() * (D + 1) * FACTOR` with a minimum of +// `FLOOR`. Two regimes so that D>=4's higher queue demand does not force a +// global budget increase. +// +// The D>=4 constants are sized from the measured `max_queue` distribution on the +// 500-point 4D seeded repro (seed `0xD225B8A07E274AE6`, ball radius 100) +// captured in `docs/archive/issue_204_investigation.md`: +// +// max_queue samples min=91 p50=207 p90=281 p95=312 p99=409 max=416 +// +// `FACTOR = 12` with `FLOOR = 96` yields a typical 300-flip budget (5-simplex seed +// set), covering p50-p90 and brushing p95. The p95-p99 tail is deferred to the +// final completion repair rather than paid for during every cadenced repair. +const LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_GE_4: usize = 12; +const LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_GE_4: usize = 96; +const LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_LT_4: usize = 4; +const LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_LT_4: usize = 16; +const LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_GE_4: usize = 24; +const LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_LT_4: usize = 16; + +/// Per-insertion local Delaunay repair flip budget. +/// +/// Computes `seeds * (D + 1) * FACTOR` with a minimum of `FLOOR`, using the +/// dimension-aware constants above. +pub(crate) const fn local_repair_flip_budget(seed_simplices_len: usize) -> usize { + let (factor, floor) = if D >= 4 { + ( + LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_GE_4, + LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_GE_4, + ) + } else { + ( + LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_LT_4, + LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_LT_4, + ) + }; + let raw = seed_simplices_len + .saturating_mul(D + 1) + .saturating_mul(factor); + if raw > floor { raw } else { floor } +} + +/// Pending local repair frontier size that triggers an early batch repair. +/// +/// The threshold keeps sparse repair cadences from letting a large seed +/// frontier accumulate. 3D uses a lower threshold because the 3000-point sweep +/// in #341 showed that repair cost rises sharply once the pending frontier +/// grows beyond the small-batch regime. +const fn local_repair_seed_backlog_threshold() -> usize { + let factor = if D >= 4 { + LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_GE_4 + } else { + LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_LT_4 + }; + (D + 1).saturating_mul(factor) +} + +/// Decides whether batch construction should run local Delaunay repair now. +pub(crate) fn batch_local_repair_trigger( + policy: DelaunayRepairPolicy, + insertion_count: usize, + topology: TopologyGuarantee, + pending_seed_simplices_len: usize, +) -> Option { + if policy == DelaunayRepairPolicy::Never + || pending_seed_simplices_len == 0 + || !TopologicalOperation::FacetFlip.is_admissible_under(topology) + { + return None; + } + + if matches!( + policy.decide(insertion_count, topology, TopologicalOperation::FacetFlip,), + RepairDecision::Proceed + ) { + return Some(BatchLocalRepairTrigger::Cadence); + } + + (pending_seed_simplices_len >= local_repair_seed_backlog_threshold::()) + .then_some(BatchLocalRepairTrigger::SeedBacklog) +} + // ============================================================================= // BATCH CONSTRUCTION ORDERING HELPERS (INTERNAL) // ============================================================================= type VertexBuffer = Vec>; -struct PreprocessVertices { + +pub(crate) struct PreprocessVertices { primary: Option>, fallback: Option>, grid_cell_size: Option, @@ -1444,19 +934,22 @@ struct PreprocessVertices { impl PreprocessVertices { /// Borrows the preprocessed vertex order when one exists, avoiding a clone /// for policies that leave the input unchanged. - fn primary_slice<'a>(&'a self, input: &'a [Vertex]) -> &'a [Vertex] { + pub(crate) fn primary_slice<'a>( + &'a self, + input: &'a [Vertex], + ) -> &'a [Vertex] { self.primary.as_deref().unwrap_or(input) } /// Exposes the original order as a retry fallback for balanced-simplex /// preprocessing. - fn fallback_slice(&self) -> Option<&[Vertex]> { + pub(crate) fn fallback_slice(&self) -> Option<&[Vertex]> { self.fallback.as_deref() } /// Carries the dedup grid size forward so incremental insertion can reuse a /// compatible spatial index. - const fn grid_cell_size(&self) -> Option + pub(crate) const fn grid_cell_size(&self) -> Option where T: Copy, { @@ -1464,7 +957,7 @@ impl PreprocessVertices { } } -type PreprocessVerticesResult = +pub(crate) type PreprocessVerticesResult = Result, DelaunayTriangulationConstructionError>; /// Hashes coordinates as a deterministic tiebreaker for partial vertex ordering. @@ -1547,7 +1040,7 @@ where /// Provides a scalar-aware tolerance for dedup paths that need a nonzero grid /// size even under exact duplicate policy. -fn default_duplicate_tolerance() -> T { +pub(crate) fn default_duplicate_tolerance() -> T { ::from(1e-10_f64).unwrap_or_else(T::default_tolerance) } @@ -1869,6 +1362,8 @@ fn push_unique_index(indices: &mut Vec, idx: usize) { } /// Computes the bounded candidate-pool size for max-volume simplex search. +const INITIAL_SIMPLEX_MAX_VOLUME_CANDIDATE_CAP: usize = 18; + const fn initial_simplex_candidate_cap(point_count: usize) -> usize { let minimum = D.saturating_add(1); let bounded_cap = if INITIAL_SIMPLEX_MAX_VOLUME_CANDIDATE_CAP > minimum { @@ -2243,235 +1738,66 @@ fn hilbert_bits_per_coord() -> Option { Some(bits_per_coord) } -/// Reads the optional batch-construction progress cadence from the environment. +/// Converts vertex coordinates for diagnostics without synthesizing sentinel values. /// -/// `DELAUNAY_BULK_PROGRESS_EVERY` is the canonical knob. The large-scale debug -/// harness also reuses `DELAUNAY_LARGE_DEBUG_PROGRESS_EVERY` so manual runs can -/// request periodic progress without additional wiring. -fn bulk_progress_every_from_env() -> Option { - [ - "DELAUNAY_BULK_PROGRESS_EVERY", - "DELAUNAY_LARGE_DEBUG_PROGRESS_EVERY", - ] - .into_iter() - .find_map(|name| { - env::var(name) - .ok() - .and_then(|raw| raw.trim().parse::().ok()) - }) - .filter(|every| *every > 0) +/// Returns `None` if any coordinate cannot be represented as `f64`, allowing +/// callers to omit diagnostic coordinates instead of hiding conversion failure +/// behind `NaN` or infinity. +pub(crate) fn vertex_coords_f64(vertex: &Vertex) -> Option> +where + T: CoordinateScalar, +{ + vertex + .point() + .coords() + .iter() + .map(|coord| coord.to_f64().filter(|value| value.is_finite())) + .collect() } -/// Enables release-visible retry-boundary tracing for bulk construction. -fn construction_retry_trace_enabled() -> bool { - bulk_progress_every_from_env().is_some() - || env::var_os("DELAUNAY_DEBUG_SHUFFLE").is_some() - || env::var_os("DELAUNAY_INSERT_TRACE").is_some() -} +/// Sort key for Hilbert ordering: `(hilbert_index, quantized_coords, vertex, input_index)`. +type HilbertSortKey = (u128, [u32; D], Vertex, usize); -/// Converts a measured duration to nanoseconds while saturating pathological -/// values that exceed the public telemetry counter width. -fn duration_nanos_saturating(duration: Duration) -> u64 { - u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX) -} +/// Orders vertices along a Hilbert curve to improve insertion locality while +/// retaining deterministic lexicographic fallbacks. +fn order_vertices_hilbert( + vertices: Vec>, + dedup_quantized: bool, +) -> Vec> +where + T: CoordinateScalar, +{ + if vertices.is_empty() || D == 0 { + return vertices; + } -#[derive(Clone, Copy, Debug)] -/// Snapshot of one batch-construction progress sample. -struct BatchProgressSample { - bulk_processed: usize, - bulk_inserted: usize, - bulk_skipped: usize, - simplex_count: usize, - perturbation_seed: u64, -} + let Some(bits_per_coord) = hilbert_bits_per_coord::() else { + return order_vertices_lexicographic(vertices); + }; -#[derive(Clone, Copy, Debug)] -/// Rolling state used to compute periodic batch throughput summaries. -struct BatchProgressState { - input_vertices: usize, - initial_simplex_vertices: usize, - bulk_vertices: usize, - progress_every: usize, - started: Instant, - last_progress: Instant, - last_processed: usize, -} + // Compute global bounds in f64 for normalization. If any coordinate is non-finite, + // fall back to lexicographic ordering (Hilbert normalization assumes finite values). + let mut min = f64::INFINITY; + let mut max = f64::NEG_INFINITY; -/// Emits periodic batch-construction progress for long-running release-mode -/// investigations such as the 4D large-scale debug harness. -/// -/// Progress is emitted via `tracing::debug!`; enable with `RUST_LOG=debug` (the -/// large-scale debug harness wires this up automatically when -/// `DELAUNAY_BULK_PROGRESS_EVERY` is set). -fn log_bulk_progress_if_due(sample: BatchProgressSample, state: &mut Option) { - let Some(state) = state.as_mut() else { - return; - }; - if sample.bulk_processed == 0 { - return; + for v in &vertices { + for &coord in v.point().coords() { + let Some(c) = coord.to_f64() else { + return order_vertices_lexicographic(vertices); + }; + if !c.is_finite() { + return order_vertices_lexicographic(vertices); + } + min = min.min(c); + max = max.max(c); + } } - // Always log the final sample, even when the total is not an exact multiple of the - // requested cadence, so interrupted runs still end with a complete progress line. - let should_log = sample.bulk_processed == state.bulk_vertices - || sample.bulk_processed.is_multiple_of(state.progress_every); - if !should_log { - return; - } + let (Some(min_t), Some(max_t)) = (NumCast::from(min), NumCast::from(max)) else { + return order_vertices_lexicographic(vertices); + }; - let elapsed = state.started.elapsed(); - let chunk_elapsed = state.last_progress.elapsed(); - let chunk_processed = sample.bulk_processed.saturating_sub(state.last_processed); - - let overall_rate = safe_usize_to_scalar::(sample.bulk_processed) - .ok() - .map(|processed| processed / elapsed.as_secs_f64().max(1e-9)); - let chunk_rate = safe_usize_to_scalar::(chunk_processed) - .ok() - .map(|processed| processed / chunk_elapsed.as_secs_f64().max(1e-9)); - - tracing::debug!( - target: "delaunay::bulk_progress", - perturbation_seed = format_args!("0x{:X}", sample.perturbation_seed), - input_vertices = state.input_vertices, - initial_simplex_vertices = state.initial_simplex_vertices, - bulk_processed = sample.bulk_processed, - bulk_vertices = state.bulk_vertices, - bulk_inserted = sample.bulk_inserted, - bulk_skipped = sample.bulk_skipped, - simplices = sample.simplex_count, - elapsed = ?elapsed, - total_rate_pts_per_s = ?overall_rate, - recent_rate_pts_per_s = ?chunk_rate, - "bulk-construction progress" - ); - - state.last_progress = Instant::now(); - state.last_processed = sample.bulk_processed; -} - -/// Emits retry-boundary events for release-mode large-scale construction runs. -fn log_construction_retry_start(attempt: usize, attempt_seed: u64, perturbation_seed: u64) { - if !construction_retry_trace_enabled() { - return; - } - - tracing::debug!( - target: "delaunay::bulk_retry", - attempt, - attempt_seed = format_args!("0x{:X}", attempt_seed), - perturbation_seed = format_args!("0x{:X}", perturbation_seed), - "shuffled retry attempt starting" - ); -} - -/// Emits retry attempt outcomes with optional construction statistics. -fn log_construction_retry_result( - attempt: usize, - attempt_seed: Option, - perturbation_seed: u64, - outcome: &'static str, - error: Option<&str>, - stats: Option<&ConstructionStatistics>, -) { - if !construction_retry_trace_enabled() { - return; - } - - let attempt_seed_display = - attempt_seed.map_or_else(|| String::from("input-order"), |seed| format!("0x{seed:X}")); - let error_display = error.unwrap_or("-"); - - if let Some(stats) = stats { - tracing::debug!( - target: "delaunay::bulk_retry", - attempt, - attempt_seed = %attempt_seed_display, - perturbation_seed = format_args!("0x{:X}", perturbation_seed), - outcome, - inserted = stats.inserted, - skipped_duplicate = stats.skipped_duplicate, - skipped_degeneracy = stats.skipped_degeneracy, - total_attempts = stats.total_attempts, - max_attempts = stats.max_attempts, - simplices_removed_total = stats.simplices_removed_total, - simplices_removed_max = stats.simplices_removed_max, - error = %error_display, - "shuffled retry attempt result (with stats)" - ); - } else { - tracing::debug!( - target: "delaunay::bulk_retry", - attempt, - attempt_seed = %attempt_seed_display, - perturbation_seed = format_args!("0x{:X}", perturbation_seed), - outcome, - error = %error_display, - "shuffled retry attempt result" - ); - } -} - -/// Converts vertex coordinates for diagnostics without synthesizing sentinel values. -/// -/// Returns `None` if any coordinate cannot be represented as `f64`, allowing -/// callers to omit diagnostic coordinates instead of hiding conversion failure -/// behind `NaN` or infinity. -fn vertex_coords_f64(vertex: &Vertex) -> Option> -where - T: CoordinateScalar, -{ - vertex - .point() - .coords() - .iter() - .map(|coord| coord.to_f64().filter(|value| value.is_finite())) - .collect() -} - -/// Sort key for Hilbert ordering: `(hilbert_index, quantized_coords, vertex, input_index)`. -type HilbertSortKey = (u128, [u32; D], Vertex, usize); - -/// Orders vertices along a Hilbert curve to improve insertion locality while -/// retaining deterministic lexicographic fallbacks. -fn order_vertices_hilbert( - vertices: Vec>, - dedup_quantized: bool, -) -> Vec> -where - T: CoordinateScalar, -{ - if vertices.is_empty() || D == 0 { - return vertices; - } - - let Some(bits_per_coord) = hilbert_bits_per_coord::() else { - return order_vertices_lexicographic(vertices); - }; - - // Compute global bounds in f64 for normalization. If any coordinate is non-finite, - // fall back to lexicographic ordering (Hilbert normalization assumes finite values). - let mut min = f64::INFINITY; - let mut max = f64::NEG_INFINITY; - - for v in &vertices { - for &coord in v.point().coords() { - let Some(c) = coord.to_f64() else { - return order_vertices_lexicographic(vertices); - }; - if !c.is_finite() { - return order_vertices_lexicographic(vertices); - } - min = min.min(c); - max = max.max(c); - } - } - - let (Some(min_t), Some(max_t)) = (NumCast::from(min), NumCast::from(max)) else { - return order_vertices_lexicographic(vertices); - }; - - let bounds = (min_t, max_t); + let bounds = (min_t, max_t); // Phase 1: Quantize all coordinates let quantized: Result, ()> = vertices @@ -2546,138 +1872,21 @@ where } } -/// Delaunay triangulation with incremental insertion support. -/// -/// # Type Parameters -/// - `K`: Geometric kernel implementing predicates -/// - `U`: User data type for vertices -/// - `V`: User data type for simplices -/// - `D`: Dimension of the triangulation -/// -/// # Delaunay Property Note -/// -/// The triangulation satisfies **structural validity** (all TDS invariants) and -/// uses **flip-based repairs** to restore the local Delaunay property after insertion. -/// By default, k=2/k=3 bistellar flip queues run automatically after each successful -/// insertion (see [`DelaunayRepairPolicy`]). -/// -/// For applications requiring explicit verification, you can still call -/// [`is_valid`](Self::is_valid) (Level 4) or [`validate`](Self::validate) (Levels 1–4). -/// If flip-based repair fails to converge, insertion returns an error and the -/// triangulation is left structurally valid but not guaranteed Delaunay. -/// -/// See: [Issue #120 Investigation](https://github.com/acgetchell/delaunay/blob/main/docs/archive/issue_120_investigation.md) -/// -/// # Implementation -/// -/// 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`] -/// - ✅ 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::triangulation::fill_cavity -/// [`wire_cavity_neighbors`]: crate::triangulation::wire_cavity_neighbors -/// [`extend_hull`]: crate::triangulation::extend_hull -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -/// -/// 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::new(&vertices).unwrap(); -/// -/// assert_eq!(dt.number_of_simplices(), 1); -/// ``` -#[derive(Clone, Debug)] -pub struct DelaunayTriangulation, U, V, const D: usize> { - /// The underlying generic triangulation. - pub(crate) tri: Triangulation, - /// Ephemeral insertion/repair state (hint caching + repair scheduling). - insertion_state: DelaunayInsertionState, - /// Optional spatial hash-grid index used to accelerate duplicate detection and locate-hint - /// selection during incremental insertion. - /// - /// This is a performance-only cache and is not serialized; it may be rebuilt lazily. - /// Query paths validate returned vertex keys against the live TDS, so the - /// cache can survive transactional rollbacks even if they leave behind stale - /// keys from an insertion that did not commit. - spatial_index: Option>, -} - -// Most common case: f64 with AdaptiveKernel, no vertex or simplex data +// Most common case: f64 with AdaptiveKernel, no vertex or simplex data. impl DelaunayTriangulation, (), (), D> { - /// Create a Delaunay triangulation from vertices with no data (most common case). - /// - /// This is the simplest constructor for the most common use case: - /// - f64 coordinates - /// - Adaptive precision predicates with Simulation of Simplicity (`SoS`) - /// - No vertex data - /// - No simplex data - /// - /// No type annotations needed! The compiler can infer everything. - /// - /// # Advanced Configuration - /// - /// For advanced use cases requiring custom construction options, topology guarantees, - /// or toroidal (periodic) triangulations, use [`DelaunayTriangulationBuilder`]: + /// Creates a Delaunay triangulation from `f64` vertices with no attached data. /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulationBuilder, TopologyGuarantee, vertex, - /// }; - /// - /// 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]), - /// ]; - /// - /// // Advanced: custom topology guarantee - /// let dt = DelaunayTriangulationBuilder::new(&vertices) - /// .topology_guarantee(TopologyGuarantee::Pseudomanifold) - /// .build::<()>() - /// .unwrap(); - /// ``` - /// - /// For toroidal (periodic) triangulations: - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulationBuilder, vertex, - /// }; - /// - /// let vertices = vec![ - /// vertex!([0.1, 0.2]), - /// vertex!([0.8, 0.3]), - /// vertex!([0.5, 0.7]), - /// ]; - /// - /// // Advanced: toroidal (periodic) triangulation - /// let dt = DelaunayTriangulationBuilder::new(&vertices) - /// .toroidal([1.0, 1.0]) // Phase 1: canonicalization - /// .build::<()>() - /// .unwrap(); - /// ``` + /// This convenience constructor uses [`AdaptiveKernel`] and the default + /// construction options. /// /// # Errors - /// Returns error if initial simplex cannot be constructed or insertion fails. + /// Returns an error if the initial simplex cannot be constructed, if + /// insertion fails, or if final validation fails. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -2686,7 +1895,6 @@ impl DelaunayTriangulation, (), (), D> { /// vertex!([0.0, 0.0, 1.0]), /// ]; /// - /// // No type annotations needed! /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); /// assert_eq!(dt.number_of_vertices(), 4); /// ``` @@ -2696,24 +1904,18 @@ impl DelaunayTriangulation, (), (), D> { Self::with_kernel(&AdaptiveKernel::::new(), vertices) } - /// Create a Delaunay triangulation and return aggregate construction statistics. - /// - /// This is identical to [`new`](Self::new) (including default [`ConstructionOptions`]) but also - /// returns a [`ConstructionStatistics`] summary of the insertion attempts performed during - /// batch construction. + /// Creates a default `f64` triangulation and returns aggregate construction + /// statistics. /// /// # Errors - /// Returns [`DelaunayTriangulationConstructionErrorWithStatistics`] if construction fails. - /// The returned error includes the partial [`ConstructionStatistics`] collected up to the - /// failure point. + /// Returns [`DelaunayTriangulationConstructionErrorWithStatistics`] if + /// construction fails. The error includes partial statistics collected + /// before failure. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayConstructionFailure, DelaunayTriangulation, - /// DelaunayTriangulationConstructionError, vertex, - /// }; + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -2726,17 +1928,6 @@ impl DelaunayTriangulation, (), (), D> { /// .unwrap(); /// assert_eq!(dt.number_of_vertices(), 4); /// assert_eq!(stats.inserted, 4); - /// - /// let invalid = vec![vertex!([0.0, 0.0, 0.0])]; - /// let err = DelaunayTriangulation::new_with_construction_statistics(&invalid) - /// .unwrap_err(); - /// assert!(matches!( - /// err.error, - /// DelaunayTriangulationConstructionError::Triangulation( - /// DelaunayConstructionFailure::InsufficientVertices { dimension: 3, .. } - /// ) - /// )); - /// assert_eq!(err.statistics.inserted, 0); /// ``` #[expect( clippy::result_large_err, @@ -2755,20 +1946,41 @@ impl DelaunayTriangulation, (), (), D> { ) } - /// Create a Delaunay triangulation with explicit batch-construction options and return - /// aggregate construction statistics. + /// Creates a default `f64` triangulation with explicit construction options + /// and returns aggregate construction statistics. /// /// # Errors - /// Returns [`DelaunayTriangulationConstructionErrorWithStatistics`] if construction fails. - /// The returned error includes the partial [`ConstructionStatistics`] collected up to the - /// failure point. + /// Returns [`DelaunayTriangulationConstructionErrorWithStatistics`] if + /// construction fails. The error includes partial statistics collected + /// before failure. /// /// # Examples /// - /// See [`new_with_construction_statistics`](Self::new_with_construction_statistics) - /// for a runnable example showing both tuple unpacking and failure - /// inspection. This constructor has the same return shape, with explicit - /// [`ConstructionOptions`]. + /// ```rust + /// use delaunay::prelude::construction::{ + /// ConstructionOptions, DelaunayTriangulation, RetryPolicy, vertex, + /// }; + /// use std::num::NonZeroUsize; + /// + /// 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 options = ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { + /// attempts: NonZeroUsize::new(2).unwrap(), + /// base_seed: Some(7), + /// }); + /// let (dt, stats) = + /// DelaunayTriangulation::new_with_options_and_construction_statistics( + /// &vertices, + /// options, + /// ) + /// .unwrap(); + /// assert_eq!(dt.number_of_vertices(), stats.inserted); + /// ``` #[expect( clippy::result_large_err, reason = "Public API intentionally returns by-value construction statistics for compatibility" @@ -2782,10 +1994,7 @@ impl DelaunayTriangulation, (), (), D> { Self::with_options_and_statistics(&kernel, vertices, TopologyGuarantee::DEFAULT, options) } - /// Create a Delaunay triangulation with explicit batch-construction options (fast-kernel convenience). - /// - /// This is an additive API over [`new`](Self::new): it allows callers to override the default - /// batch-construction options (insertion ordering, deduplication, retry policy). + /// Creates a default `f64` triangulation with explicit construction options. /// /// # Errors /// Returns an error if construction fails, or if the selected options are invalid. @@ -2793,7 +2002,7 @@ impl DelaunayTriangulation, (), (), D> { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// ConstructionOptions, DedupPolicy, DelaunayTriangulation, InsertionOrderStrategy, vertex, /// }; /// @@ -2803,7 +2012,6 @@ impl DelaunayTriangulation, (), (), D> { /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// /// let options = ConstructionOptions::default() /// .with_insertion_order(InsertionOrderStrategy::Hilbert) /// .with_dedup_policy(DedupPolicy::Exact); @@ -2824,36 +2032,26 @@ impl DelaunayTriangulation, (), (), D> { ) } - /// Create a Delaunay triangulation with an explicit topology guarantee (fast-kernel convenience). - /// - /// The default topology guarantee is [`TopologyGuarantee::PLManifold`]. Use this - /// constructor to override it (e.g. relax to [`TopologyGuarantee::Pseudomanifold`] - /// for speed at the cost of weaker topology guarantees). + /// Creates a default `f64` triangulation with an explicit topology guarantee. /// /// # Errors - /// Returns error if construction fails or if the requested topology guarantee - /// cannot be satisfied. + /// Returns an error if construction fails or if the requested topology + /// guarantee cannot be satisfied. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulation, TopologyGuarantee, vertex, /// }; /// - /// 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 vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; /// let dt = DelaunayTriangulation::new_with_topology_guarantee( /// &vertices, - /// TopologyGuarantee::Pseudomanifold, + /// TopologyGuarantee::PLManifold, /// ) /// .unwrap(); - /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); + /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); /// ``` pub fn new_with_topology_guarantee( vertices: &[Vertex], @@ -2863,53 +2061,33 @@ impl DelaunayTriangulation, (), (), D> { Self::with_topology_guarantee(&kernel, vertices, topology_guarantee) } - /// Create an empty Delaunay triangulation with no data (most common case). - /// - /// Use this when you want to build a triangulation incrementally by inserting vertices - /// one at a time. The triangulation will automatically bootstrap itself when you - /// insert the (D+1)th vertex, creating the initial simplex. - /// - /// No type annotations needed! The compiler can infer everything. + /// Creates an empty default `f64` triangulation with no attached data. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::construction::DelaunayTriangulation; /// - /// // Start with empty triangulation - /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); /// assert_eq!(dt.number_of_vertices(), 0); /// assert_eq!(dt.number_of_simplices(), 0); - /// - /// // Insert vertices one by one - /// dt.insert(vertex!([0.0, 0.0, 0.0])).unwrap(); - /// dt.insert(vertex!([1.0, 0.0, 0.0])).unwrap(); - /// dt.insert(vertex!([0.0, 1.0, 0.0])).unwrap(); - /// dt.insert(vertex!([0.0, 0.0, 1.0])).unwrap(); // Initial simplex created automatically - /// assert_eq!(dt.number_of_simplices(), 1); /// ``` #[must_use] pub fn empty() -> Self { Self::with_empty_kernel(AdaptiveKernel::::new()) } - /// Create an empty Delaunay triangulation with an explicit topology guarantee (fast-kernel convenience). - /// - /// The default topology guarantee is [`TopologyGuarantee::PLManifold`]. Use this - /// constructor to override it (e.g. relax to [`TopologyGuarantee::Pseudomanifold`] - /// for speed at the cost of weaker topology guarantees). + /// Creates an empty default `f64` triangulation with an explicit topology guarantee. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ + /// use delaunay::prelude::construction::{ /// DelaunayTriangulation, TopologyGuarantee, /// }; /// /// let dt: DelaunayTriangulation<_, (), (), 3> = /// DelaunayTriangulation::empty_with_topology_guarantee(TopologyGuarantee::Pseudomanifold); - /// - /// assert_eq!(dt.number_of_vertices(), 0); /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); /// ``` #[must_use] @@ -2920,57 +2098,20 @@ impl DelaunayTriangulation, (), (), D> { ) } - /// Create a fluent builder for constructing a Delaunay triangulation. + /// Creates a fluent builder for default `f64` Delaunay triangulations. /// - /// This is a convenience entry point that produces a - /// [`DelaunayTriangulationBuilder`] - /// pre-typed for `f64` coordinates, no vertex data (`()`), and dimension `D`. - /// - /// For non-`f64` coordinates, vertex data (`U ≠ ()`), or custom kernels, construct - /// `DelaunayTriangulationBuilder::new(vertices)` directly. + /// For non-`f64` coordinates, vertex data, or custom kernels, construct + /// [`DelaunayTriangulationBuilder::new`] directly. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulation, DelaunayTriangulationBuilder, vertex, - /// }; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// - /// let dt = DelaunayTriangulation::builder(&vertices) - /// .build::<()>() - /// .unwrap(); + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; /// + /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; + /// let dt = DelaunayTriangulation::builder(&vertices).build::<()>().unwrap(); /// assert_eq!(dt.number_of_vertices(), 3); /// ``` - /// - /// ## Toroidal construction - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulation, DelaunayTriangulationBuilder, vertex, - /// }; - /// - /// // Vertices outside [0, 1)² are canonicalized before building. - /// let vertices = vec![ - /// vertex!([0.2, 0.3]), - /// vertex!([1.8, 0.1]), // wraps to (0.8, 0.1) - /// vertex!([0.5, 0.7]), - /// vertex!([-0.4, 0.9]), // wraps to (0.6, 0.9) - /// ]; - /// - /// let dt = DelaunayTriangulation::builder(&vertices) - /// .toroidal([1.0, 1.0]) - /// .build::<()>() - /// .unwrap(); - /// - /// assert_eq!(dt.number_of_vertices(), 4); - /// ``` #[must_use] pub fn builder( vertices: &[Vertex], @@ -2994,755 +2135,400 @@ where U: DataType, V: DataType, { - /// Create an empty Delaunay triangulation with the given kernel (advanced usage). - /// - /// Most users should use [`DelaunayTriangulation::empty()`] instead, which uses fast predicates - /// by default. Use this method only if you need custom coordinate precision or specialized kernels. - /// - /// This creates a triangulation with no vertices or simplices. Use [`insert`](Self::insert) - /// to add vertices incrementally. The triangulation will automatically bootstrap itself when - /// you insert the (D+1)th vertex, creating the initial simplex. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::RobustKernel; - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// // Start with empty triangulation using robust kernel - /// let mut dt: DelaunayTriangulation, (), (), 4> = - /// DelaunayTriangulation::with_empty_kernel(RobustKernel::new()); - /// assert_eq!(dt.number_of_vertices(), 0); - /// assert_eq!(dt.number_of_simplices(), 0); - /// - /// // Insert vertices incrementally - /// dt.insert(vertex!([0.0, 0.0, 0.0, 0.0])).unwrap(); - /// dt.insert(vertex!([1.0, 0.0, 0.0, 0.0])).unwrap(); - /// dt.insert(vertex!([0.0, 1.0, 0.0, 0.0])).unwrap(); - /// dt.insert(vertex!([0.0, 0.0, 1.0, 0.0])).unwrap(); - /// dt.insert(vertex!([0.0, 0.0, 0.0, 1.0])).unwrap(); // Initial simplex created - /// assert_eq!(dt.number_of_simplices(), 1); - /// ``` - #[must_use] - pub fn with_empty_kernel(kernel: K) -> Self { - let duplicate_tolerance = default_duplicate_tolerance::(); - - Self { - tri: Triangulation::new_empty(kernel), - insertion_state: DelaunayInsertionState::new(), - spatial_index: Some(HashGridIndex::new(duplicate_tolerance)), - } - } - - /// Create an empty Delaunay triangulation with the given kernel and topology guarantee. - /// - /// This is the kernel-parameterized variant of - /// [`DelaunayTriangulation::empty_with_topology_guarantee`]. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::RobustKernel; - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulation, TopologyGuarantee, vertex, - /// }; - /// - /// let dt: DelaunayTriangulation, (), (), 3> = - /// DelaunayTriangulation::with_empty_kernel_and_topology_guarantee( - /// RobustKernel::new(), - /// TopologyGuarantee::PLManifold, - /// ); - /// - /// assert_eq!(dt.number_of_vertices(), 0); - /// ``` - #[must_use] - pub fn with_empty_kernel_and_topology_guarantee( - kernel: K, - topology_guarantee: TopologyGuarantee, - ) -> Self { - let duplicate_tolerance = default_duplicate_tolerance::(); - - let mut tri = Triangulation::new_empty(kernel); - tri.set_topology_guarantee(topology_guarantee); - Self { - tri, - insertion_state: DelaunayInsertionState::new(), - spatial_index: Some(HashGridIndex::new(duplicate_tolerance)), - } - } - - /// Create a Delaunay triangulation from vertices with an explicit kernel (advanced usage). - /// - /// Most users should use [`DelaunayTriangulation::new()`] instead, which uses the - /// [`AdaptiveKernel`] by default. Use this method when you need a different kernel: - /// - /// - **[`RobustKernel`]** — exact Bareiss arithmetic (recommended for correctness) - /// - **[`FastKernel`](crate::geometry::kernel::FastKernel)** — raw `f64` (faster, - /// but may produce incorrect results for near-degenerate inputs) - /// - Custom coordinate precision (f32, custom types) - /// - /// **Note:** `FastKernel` is accepted for construction and insertion, but the - /// explicit repair methods ([`repair_delaunay_with_flips`](Self::repair_delaunay_with_flips), - /// [`repair_delaunay_with_flips_advanced`](Self::repair_delaunay_with_flips_advanced)) - /// require [`ExactPredicates`] and are not available for `FastKernel`. - /// - /// This uses the efficient cavity-based algorithm: - /// 1. Build initial simplex (D+1 vertices) directly - /// 2. Insert remaining vertices incrementally with locate → conflict → cavity → wire - /// - /// # Errors - /// Returns error if initial simplex cannot be constructed or insertion fails. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::RobustKernel; - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 0.0, 1.0]), - /// ]; - /// - /// // Use robust kernel for exact arithmetic - /// let kernel = RobustKernel::new(); - /// let dt: DelaunayTriangulation, (), (), 4> = - /// DelaunayTriangulation::with_kernel(&kernel, &vertices).unwrap(); - /// assert_eq!(dt.number_of_vertices(), 5); - /// ``` - pub fn with_kernel( - kernel: &K, - vertices: &[Vertex], - ) -> Result { - Self::with_topology_guarantee(kernel, vertices, TopologyGuarantee::DEFAULT) - } - - /// Create a Delaunay triangulation with an explicit topology guarantee. - /// - /// Passing [`TopologyGuarantee::PLManifold`] enforces ridge-link validation during - /// construction and validates vertex-links at completion. Use - /// [`TopologyGuarantee::PLManifoldStrict`] for per-insertion vertex-link checks. - /// - /// # Shuffled Retries - /// For `D >= 2` with more than `D + 1` vertices, the constructor retries - /// construction with up to 6 shuffled insertion orders if the Delaunay - /// property check fails (see [`RetryPolicy::default()`]). To disable - /// retries, pass [`ConstructionOptions::default().with_retry_policy(RetryPolicy::Disabled)`](Self::with_topology_guarantee_and_options). - /// - /// # Errors - /// Returns error if construction fails or if the requested topology guarantee - /// cannot be satisfied. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::RobustKernel; - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulation, TopologyGuarantee, vertex, - /// }; - /// - /// 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 kernel = RobustKernel::new(); - /// let dt: DelaunayTriangulation, (), (), 3> = - /// DelaunayTriangulation::with_topology_guarantee( - /// &kernel, - /// &vertices, - /// TopologyGuarantee::PLManifold, - /// ) - /// .unwrap(); - /// assert_eq!(dt.number_of_vertices(), 4); - /// ``` - pub fn with_topology_guarantee( + /// Retries batch construction with deterministic shuffles so retryable + /// degeneracies can be escaped reproducibly. + #[expect( + clippy::too_many_lines, + reason = "construction retry flow keeps seed selection, validation, and diagnostics together" + )] + #[expect( + clippy::too_many_arguments, + reason = "private construction retry helper threads orthogonal batch knobs explicitly" + )] + pub(crate) fn build_with_shuffled_retries( kernel: &K, vertices: &[Vertex], topology_guarantee: TopologyGuarantee, + attempts: NonZeroUsize, + base_seed: Option, + grid_cell_size: Option, + batch_repair_policy: DelaunayRepairPolicy, + use_global_repair_fallback: bool, ) -> Result { - Self::with_topology_guarantee_and_options( - kernel, + let base_seed = base_seed.unwrap_or_else(|| Self::construction_shuffle_seed(vertices)); + + #[cfg(debug_assertions)] + let log_shuffle = env::var_os("DELAUNAY_DEBUG_SHUFFLE").is_some(); + + #[cfg(debug_assertions)] + if log_shuffle { + tracing::debug!( + base_seed, + attempts = attempts.get(), + vertex_count = vertices.len(), + "build_with_shuffled_retries: starting" + ); + } + + // Attempt 0: original order, no extra perturbation salt. + log_construction_retry_result(0, None, 0_u64, "started", None, None); + let mut last_error: String = match Self::build_with_kernel_inner_seeded( + ::clone(kernel), vertices, topology_guarantee, - ConstructionOptions::default(), - ) - } - - /// Create a Delaunay triangulation with an explicit topology guarantee and batch-construction options. - /// - /// This is the core constructor used by the higher-level convenience constructors. It allows callers - /// to opt into deterministic preprocessing and retry behavior. - /// - /// # Errors - /// Returns an error if: - /// - construction fails or the requested topology guarantee cannot be satisfied, or - /// - the selected preprocessing options are invalid (e.g. a negative / non-finite epsilon tolerance). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::RobustKernel; - /// use delaunay::prelude::triangulation::construction::{ - /// ConstructionOptions, DedupPolicy, DelaunayTriangulation, InsertionOrderStrategy, - /// TopologyGuarantee, vertex, - /// }; - /// - /// 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 options = ConstructionOptions::default() - /// .with_insertion_order(InsertionOrderStrategy::Hilbert) - /// .with_dedup_policy(DedupPolicy::Off); - /// - /// let kernel = RobustKernel::new(); - /// let dt: DelaunayTriangulation, (), (), 3> = - /// DelaunayTriangulation::with_topology_guarantee_and_options( - /// &kernel, - /// &vertices, - /// TopologyGuarantee::PLManifold, - /// options, - /// ) - /// .unwrap(); - /// assert_eq!(dt.number_of_simplices(), 1); - /// ``` - pub fn with_topology_guarantee_and_options( - kernel: &K, - vertices: &[Vertex], - topology_guarantee: TopologyGuarantee, - options: ConstructionOptions, - ) -> Result { - let ConstructionOptions { - insertion_order, - dedup_policy, - initial_simplex, - retry_policy, + 0_u64, + true, + grid_cell_size, batch_repair_policy, use_global_repair_fallback, - } = options; - - let preprocessed = Self::preprocess_vertices_for_construction( - vertices, - dedup_policy, - insertion_order, - initial_simplex, - )?; - let grid_cell_size = preprocessed.grid_cell_size(); - let primary_vertices: &[Vertex] = preprocessed.primary_slice(vertices); - let fallback_vertices = preprocessed.fallback_slice(); - - let build_with_vertices = |vertices: &[Vertex]| { - match retry_policy { - RetryPolicy::Disabled => {} - RetryPolicy::Shuffled { - attempts, - base_seed, - } => { - if Self::should_retry_construction(vertices) { - return Self::build_with_shuffled_retries( - kernel, - vertices, - topology_guarantee, - attempts, - base_seed, - grid_cell_size, - batch_repair_policy, - use_global_repair_fallback, - ); - } + ) { + Ok(candidate) => match candidate.is_delaunay_via_flips() { + Ok(()) => { + log_construction_retry_result(0, None, 0_u64, "succeeded", None, None); + return Ok(candidate); } - RetryPolicy::DebugOnlyShuffled { - attempts, - base_seed, - } => { - if cfg!(any(test, debug_assertions)) - && Self::should_retry_construction(vertices) - { - return Self::build_with_shuffled_retries( - kernel, - vertices, - topology_guarantee, - attempts, - base_seed, - grid_cell_size, - batch_repair_policy, - use_global_repair_fallback, - ); - } + Err(err) => format!("Delaunay property violated after construction: {err}"), + }, + Err(err) => { + let err_string = err.to_string(); + if Self::is_non_retryable_construction_error(&err) { + log_construction_retry_result( + 0, + None, + 0_u64, + "failed", + Some(&err_string), + None, + ); + return Err(err); } + err_string } - - Self::build_with_kernel_inner( - ::clone(kernel), - vertices, - topology_guarantee, - grid_cell_size, - batch_repair_policy, - use_global_repair_fallback, - ) }; - let result = build_with_vertices(primary_vertices); - if result.is_err() - && let Some(fallback) = fallback_vertices - { - return build_with_vertices(fallback); + #[cfg(debug_assertions)] + if log_shuffle { + tracing::debug!( + attempt = 0, + perturbation_seed = 0_u64, + last_error = %last_error, + "build_with_shuffled_retries: initial attempt failed: {last_error}" + ); } + log_construction_retry_result(0, None, 0_u64, "failed", Some(&last_error), None); - result + // Shuffled retries (total iterations: attempts shuffled). + for attempt in 1..=attempts.get() { + let mut shuffled = vertices.to_vec(); + + let mut attempt_seed = + base_seed.wrapping_add((attempt as u64).wrapping_mul(DELAUNAY_SHUFFLE_SEED_SALT)); + if attempt_seed == 0 { + attempt_seed = 1; + } + + Self::shuffle_vertices(&mut shuffled, attempt_seed); + + // Vary the deterministic perturbation pattern across retry attempts. + let perturbation_seed = attempt_seed ^ 0xD1B5_4A32_D192_ED03; + + #[cfg(debug_assertions)] + if log_shuffle { + tracing::debug!( + attempt, + attempt_seed, + perturbation_seed, + "build_with_shuffled_retries: shuffled attempt starting" + ); + } + log_construction_retry_start(attempt, attempt_seed, perturbation_seed); + + match Self::build_with_kernel_inner_seeded( + ::clone(kernel), + &shuffled, + topology_guarantee, + perturbation_seed, + true, + grid_cell_size, + batch_repair_policy, + use_global_repair_fallback, + ) { + Ok(candidate) => match candidate.is_delaunay_via_flips() { + Ok(()) => { + log_construction_retry_result( + attempt, + Some(attempt_seed), + perturbation_seed, + "succeeded", + None, + None, + ); + return Ok(candidate); + } + Err(err) => { + last_error = + format!("Delaunay property violated after construction: {err}"); + } + }, + Err(err) => { + let err_string = err.to_string(); + if Self::is_non_retryable_construction_error(&err) { + log_construction_retry_result( + attempt, + Some(attempt_seed), + perturbation_seed, + "failed", + Some(&err_string), + None, + ); + return Err(err); + } + last_error = err_string; + } + } + + #[cfg(debug_assertions)] + if log_shuffle { + tracing::debug!( + attempt, + attempt_seed, + perturbation_seed, + last_error = %last_error, + "build_with_shuffled_retries: attempt failed: {last_error}" + ); + } + log_construction_retry_result( + attempt, + Some(attempt_seed), + perturbation_seed, + "failed", + Some(&last_error), + None, + ); + } + + // Treat persistent construction failures or Delaunay violations as hard construction + // errors so callers can deterministically reject. + Err(TriangulationConstructionError::GeometricDegeneracy { + message: format!( + "Delaunay construction failed after shuffled reconstruction attempts: {last_error}" + ), + } + .into()) } - /// Like [`with_topology_guarantee_and_options`](Self::with_topology_guarantee_and_options), but - /// also returns aggregate [`ConstructionStatistics`] collected during batch construction. - /// - /// # Errors - /// Returns [`DelaunayTriangulationConstructionErrorWithStatistics`] if construction fails. - /// The returned error includes the partial [`ConstructionStatistics`] collected up to the - /// failure point. - /// - /// # Examples - /// - /// See [`new_with_construction_statistics`](Self::new_with_construction_statistics) - /// for the statistics return shape and failure-inspection pattern. This - /// constructor uses the same `(triangulation, statistics)` tuple with an - /// explicit kernel, [`TopologyGuarantee`], and [`ConstructionOptions`]. + /// Mirrors shuffled retry construction while preserving per-attempt + /// statistics for callers that need skip and retry diagnostics. + #[expect( + clippy::too_many_lines, + reason = "statistics variant mirrors construction retry flow for comparable diagnostics" + )] #[expect( clippy::result_large_err, - reason = "Public API intentionally returns by-value construction statistics for compatibility" + reason = "Internal helper propagates public by-value construction-statistics error type" )] #[expect( - clippy::too_many_lines, - reason = "Statistics constructor handles preprocessing, retry, and fallback aggregation" + clippy::too_many_arguments, + reason = "statistics retry helper mirrors the non-statistics construction path" )] - pub fn with_options_and_statistics( + pub(crate) fn build_with_shuffled_retries_with_construction_statistics( kernel: &K, vertices: &[Vertex], topology_guarantee: TopologyGuarantee, - options: ConstructionOptions, + attempts: NonZeroUsize, + base_seed: Option, + grid_cell_size: Option, + batch_repair_policy: DelaunayRepairPolicy, + use_global_repair_fallback: bool, ) -> Result<(Self, ConstructionStatistics), DelaunayTriangulationConstructionErrorWithStatistics> { - let ConstructionOptions { - insertion_order, - dedup_policy, - initial_simplex, - retry_policy, - batch_repair_policy, - use_global_repair_fallback, - } = options; + let base_seed = base_seed.unwrap_or_else(|| Self::construction_shuffle_seed(vertices)); - let preprocessing_started = Instant::now(); - let preprocessed = match Self::preprocess_vertices_for_construction( - vertices, - dedup_policy, - insertion_order, - initial_simplex, - ) { - Ok(preprocessed) => preprocessed, - Err(error) => { - let mut statistics = ConstructionStatistics::default(); - statistics - .telemetry - .record_construction_preprocessing_timing(duration_nanos_saturating( - preprocessing_started.elapsed(), - )); - return Err(DelaunayTriangulationConstructionErrorWithStatistics { - error, - statistics, - }); - } - }; - let preprocessing_nanos = duration_nanos_saturating(preprocessing_started.elapsed()); - let grid_cell_size = preprocessed.grid_cell_size(); - let primary_vertices: &[Vertex] = preprocessed.primary_slice(vertices); - let fallback_vertices = preprocessed.fallback_slice(); + #[cfg(debug_assertions)] + let log_shuffle = env::var_os("DELAUNAY_DEBUG_SHUFFLE").is_some(); - let build_with_vertices = |vertices: &[Vertex]| { - match retry_policy { - RetryPolicy::Disabled => {} - RetryPolicy::Shuffled { - attempts, - base_seed, - } => { - if Self::should_retry_construction(vertices) { - return Self::build_with_shuffled_retries_with_construction_statistics( - kernel, - vertices, - topology_guarantee, - attempts, - base_seed, - grid_cell_size, - batch_repair_policy, - use_global_repair_fallback, - ); - } - } - RetryPolicy::DebugOnlyShuffled { - attempts, - base_seed, - } => { - if cfg!(any(test, debug_assertions)) - && Self::should_retry_construction(vertices) - { - return Self::build_with_shuffled_retries_with_construction_statistics( - kernel, - vertices, - topology_guarantee, - attempts, - base_seed, - grid_cell_size, - batch_repair_policy, - use_global_repair_fallback, - ); - } - } - } + #[cfg(debug_assertions)] + if log_shuffle { + tracing::debug!( + base_seed, + attempts = attempts.get(), + vertex_count = vertices.len(), + "build_with_shuffled_retries_with_construction_statistics: starting" + ); + } - Self::build_with_kernel_inner_with_construction_statistics( + let mut last_stats: Option = None; + let mut aggregate_stats = ConstructionStatistics::default(); + + // Attempt 0: original order, no extra perturbation salt. + log_construction_retry_result(0, None, 0_u64, "started", None, None); + let mut last_error: String = + match Self::build_with_kernel_inner_seeded_with_construction_statistics( ::clone(kernel), vertices, topology_guarantee, + 0_u64, + true, grid_cell_size, batch_repair_policy, use_global_repair_fallback, - ) - }; - - match build_with_vertices(primary_vertices) { - Ok((dt, mut stats)) => { - stats - .telemetry - .record_construction_preprocessing_timing(preprocessing_nanos); - Ok((dt, stats)) - } - Err(mut primary_err) => { - let Some(fallback) = fallback_vertices else { - primary_err - .statistics + ) { + Ok((candidate, mut stats)) => { + let delaunay_started = Instant::now(); + let delaunay_result = candidate.is_delaunay_via_flips(); + stats .telemetry - .record_construction_preprocessing_timing(preprocessing_nanos); - return Err(primary_err); - }; - - match build_with_vertices(fallback) { - Ok((dt, stats)) => { - let mut aggregate = primary_err.statistics; - aggregate.merge_from(&stats); - aggregate - .telemetry - .record_construction_preprocessing_timing(preprocessing_nanos); - Ok((dt, aggregate)) + .record_construction_final_delaunay_validation_timing( + duration_nanos_saturating(delaunay_started.elapsed()), + ); + match delaunay_result { + Ok(()) => { + aggregate_stats.merge_from(&stats); + log_construction_retry_result( + 0, + None, + 0_u64, + "succeeded", + None, + Some(&stats), + ); + return Ok((candidate, aggregate_stats)); + } + Err(err) => { + aggregate_stats.merge_from(&stats); + last_stats.replace(stats); + format!("Delaunay property violated after construction: {err}") + } } - Err(fallback_err) => { - let mut aggregate = primary_err.statistics; - aggregate.merge_from(&fallback_err.statistics); - aggregate - .telemetry - .record_construction_preprocessing_timing(preprocessing_nanos); - Err(DelaunayTriangulationConstructionErrorWithStatistics { - error: fallback_err.error, - statistics: aggregate, - }) + } + Err(err) => { + let DelaunayTriangulationConstructionErrorWithStatistics { error, statistics } = + err; + aggregate_stats.merge_from(&statistics); + if Self::is_non_retryable_construction_error(&error) { + let last_error = error.to_string(); + log_construction_retry_result( + 0, + None, + 0_u64, + "failed", + Some(&last_error), + Some(&statistics), + ); + return Err(DelaunayTriangulationConstructionErrorWithStatistics { + error, + statistics: aggregate_stats, + }); } + last_stats.replace(statistics); + error.to_string() } - } + }; + + #[cfg(debug_assertions)] + if log_shuffle { + tracing::debug!( + attempt = 0, + perturbation_seed = 0_u64, + last_error = %last_error, + "build_with_shuffled_retries_with_construction_statistics: initial attempt failed: {last_error}" + ); } - } + log_construction_retry_result( + 0, + None, + 0_u64, + "failed", + Some(&last_error), + last_stats.as_ref(), + ); - /// Applies deduplication, insertion ordering, and initial-simplex selection - /// before any topology is created. - fn preprocess_vertices_for_construction( - vertices: &[Vertex], - dedup_policy: DedupPolicy, - insertion_order: InsertionOrderStrategy, - initial_simplex: InitialSimplexStrategy, - ) -> PreprocessVerticesResult { - let default_tolerance = default_duplicate_tolerance::(); + // Shuffled retries (total iterations: attempts shuffled). + for attempt in 1..=attempts.get() { + let mut shuffled = vertices.to_vec(); - let mut epsilon: Option = None; - if let DedupPolicy::Epsilon { tolerance } = dedup_policy { - if !tolerance.is_finite() || tolerance < 0.0 { - return Err(TriangulationConstructionError::GeometricDegeneracy { - message: format!( - "Invalid DedupPolicy::Epsilon tolerance {tolerance:?} (must be finite and non-negative)" - ), - } - .into()); + let mut attempt_seed = + base_seed.wrapping_add((attempt as u64).wrapping_mul(DELAUNAY_SHUFFLE_SEED_SALT)); + if attempt_seed == 0 { + attempt_seed = 1; } - let Some(epsilon_value) = ::from(tolerance) else { - return Err(TriangulationConstructionError::GeometricDegeneracy { - message: format!( - "Failed to convert DedupPolicy::Epsilon tolerance {tolerance:?} into scalar type" - ), - } - .into()); - }; - epsilon = Some(epsilon_value); - } + Self::shuffle_vertices(&mut shuffled, attempt_seed); - let grid_cell_size_value = - if let (DedupPolicy::Epsilon { .. }, Some(eps)) = (dedup_policy, epsilon) { - if eps > K::Scalar::zero() { - eps - } else { - default_tolerance - } - } else { - default_tolerance - }; - let mut grid: HashGridIndex = HashGridIndex::new(grid_cell_size_value); + // Vary the deterministic perturbation pattern across retry attempts. + let perturbation_seed = attempt_seed ^ 0xD1B5_4A32_D192_ED03; - // Deduplicate first to reduce work for ordering strategies. - let mut owned_vertices: Option>> = match dedup_policy { - DedupPolicy::Off => None, - DedupPolicy::Exact => { - let vertices = vertices.to_vec(); - if hash_grid_usable_for_vertices(&grid, &vertices) { - Some(dedup_vertices_exact_hash_grid(vertices, &mut grid)) - } else { - Some(dedup_vertices_exact_sorted(vertices)) - } - } - DedupPolicy::Epsilon { .. } => { - let epsilon = epsilon.expect("epsilon validated above"); - let vertices = vertices.to_vec(); - if hash_grid_usable_for_vertices(&grid, &vertices) { - Some(dedup_vertices_epsilon_hash_grid( - vertices, epsilon, &mut grid, - )) - } else { - Some(dedup_vertices_epsilon_quantized(vertices, epsilon)) - } + #[cfg(debug_assertions)] + if log_shuffle { + tracing::debug!( + attempt, + attempt_seed, + perturbation_seed, + "build_with_shuffled_retries_with_construction_statistics: shuffled attempt starting" + ); } - }; - - owned_vertices = match insertion_order { - InsertionOrderStrategy::Input => owned_vertices, - _ => Some(order_vertices_by_strategy( - owned_vertices.unwrap_or_else(|| vertices.to_vec()), - insertion_order, - )), - }; + log_construction_retry_start(attempt, attempt_seed, perturbation_seed); - let (primary, fallback) = match initial_simplex { - InitialSimplexStrategy::First => (owned_vertices, None), - InitialSimplexStrategy::Balanced => { - let base = owned_vertices.unwrap_or_else(|| vertices.to_vec()); - if let Some(indices) = select_balanced_simplex_indices(&base) { - if let Some(reordered) = reorder_vertices_for_simplex(&base, &indices) { - (Some(reordered), Some(base)) - } else { - (Some(base), None) + match Self::build_with_kernel_inner_seeded_with_construction_statistics( + ::clone(kernel), + &shuffled, + topology_guarantee, + perturbation_seed, + true, + grid_cell_size, + batch_repair_policy, + use_global_repair_fallback, + ) { + Ok((candidate, mut stats)) => { + let delaunay_started = Instant::now(); + let delaunay_result = candidate.is_delaunay_via_flips(); + stats + .telemetry + .record_construction_final_delaunay_validation_timing( + duration_nanos_saturating(delaunay_started.elapsed()), + ); + match delaunay_result { + Ok(()) => { + aggregate_stats.merge_from(&stats); + log_construction_retry_result( + attempt, + Some(attempt_seed), + perturbation_seed, + "succeeded", + None, + Some(&stats), + ); + return Ok((candidate, aggregate_stats)); + } + Err(err) => { + aggregate_stats.merge_from(&stats); + last_stats.replace(stats); + last_error = + format!("Delaunay property violated after construction: {err}"); + } } - } else { - (Some(base), None) } - } - InitialSimplexStrategy::MaxVolume => { - let base = owned_vertices.unwrap_or_else(|| vertices.to_vec()); - if let Some(indices) = select_max_volume_simplex_indices(&base) { - if let Some(reordered) = reorder_vertices_for_simplex(&base, &indices) { - (Some(reordered), Some(base)) - } else { - (Some(base), None) + Err(err) => { + let DelaunayTriangulationConstructionErrorWithStatistics { error, statistics } = + err; + aggregate_stats.merge_from(&statistics); + if Self::is_non_retryable_construction_error(&error) { + let last_error = error.to_string(); + log_construction_retry_result( + attempt, + Some(attempt_seed), + perturbation_seed, + "failed", + Some(&last_error), + Some(&statistics), + ); + return Err(DelaunayTriangulationConstructionErrorWithStatistics { + error, + statistics: aggregate_stats, + }); } - } else { - (Some(base), None) - } - } - }; - - let final_slice = primary.as_deref().unwrap_or(vertices); - let grid_cell_size = if hash_grid_usable_for_vertices(&grid, final_slice) { - Some(grid.cell_size()) - } else { - None - }; - - Ok(PreprocessVertices { - primary, - fallback, - grid_cell_size, - }) - } - - /// Returns `true` if the construction error is deterministic and should not - /// be masked by shuffled retry logic (e.g. duplicate UUIDs, internal bugs). - const fn is_non_retryable_construction_error( - err: &DelaunayTriangulationConstructionError, - ) -> bool { - matches!( - err, - DelaunayTriangulationConstructionError::Triangulation( - DelaunayConstructionFailure::Tds { - reason: TdsConstructionFailure::DuplicateUuid { .. } - | TdsConstructionFailure::Validation { .. }, - } | DelaunayConstructionFailure::InternalInconsistency { .. } - | DelaunayConstructionFailure::DelaunayRepair { .. } - | DelaunayConstructionFailure::InsertionTopologyValidation { .. } - | DelaunayConstructionFailure::FinalTopologyValidation { .. }, - ) - ) - } - - /// Retries batch construction with deterministic shuffles so retryable - /// degeneracies can be escaped reproducibly. - #[expect( - clippy::too_many_lines, - reason = "construction retry flow keeps seed selection, validation, and diagnostics together" - )] - #[expect( - clippy::too_many_arguments, - reason = "private construction retry helper threads orthogonal batch knobs explicitly" - )] - fn build_with_shuffled_retries( - kernel: &K, - vertices: &[Vertex], - topology_guarantee: TopologyGuarantee, - attempts: NonZeroUsize, - base_seed: Option, - grid_cell_size: Option, - batch_repair_policy: DelaunayRepairPolicy, - use_global_repair_fallback: bool, - ) -> Result { - let base_seed = base_seed.unwrap_or_else(|| Self::construction_shuffle_seed(vertices)); - - #[cfg(debug_assertions)] - let log_shuffle = env::var_os("DELAUNAY_DEBUG_SHUFFLE").is_some(); - - #[cfg(debug_assertions)] - if log_shuffle { - tracing::debug!( - base_seed, - attempts = attempts.get(), - vertex_count = vertices.len(), - "build_with_shuffled_retries: starting" - ); - } - - // Attempt 0: original order, no extra perturbation salt. - log_construction_retry_result(0, None, 0_u64, "started", None, None); - let mut last_error: String = match Self::build_with_kernel_inner_seeded( - ::clone(kernel), - vertices, - topology_guarantee, - 0_u64, - true, - grid_cell_size, - batch_repair_policy, - use_global_repair_fallback, - ) { - Ok(candidate) => match candidate.is_delaunay_via_flips() { - Ok(()) => { - log_construction_retry_result(0, None, 0_u64, "succeeded", None, None); - return Ok(candidate); - } - Err(err) => format!("Delaunay property violated after construction: {err}"), - }, - Err(err) => { - let err_string = err.to_string(); - if Self::is_non_retryable_construction_error(&err) { - log_construction_retry_result( - 0, - None, - 0_u64, - "failed", - Some(&err_string), - None, - ); - return Err(err); - } - err_string - } - }; - - #[cfg(debug_assertions)] - if log_shuffle { - tracing::debug!( - attempt = 0, - perturbation_seed = 0_u64, - last_error = %last_error, - "build_with_shuffled_retries: initial attempt failed: {last_error}" - ); - } - log_construction_retry_result(0, None, 0_u64, "failed", Some(&last_error), None); - - // Shuffled retries (total iterations: attempts shuffled). - for attempt in 1..=attempts.get() { - let mut shuffled = vertices.to_vec(); - - let mut attempt_seed = - base_seed.wrapping_add((attempt as u64).wrapping_mul(DELAUNAY_SHUFFLE_SEED_SALT)); - if attempt_seed == 0 { - attempt_seed = 1; - } - - Self::shuffle_vertices(&mut shuffled, attempt_seed); - - // Vary the deterministic perturbation pattern across retry attempts. - let perturbation_seed = attempt_seed ^ 0xD1B5_4A32_D192_ED03; - - #[cfg(debug_assertions)] - if log_shuffle { - tracing::debug!( - attempt, - attempt_seed, - perturbation_seed, - "build_with_shuffled_retries: shuffled attempt starting" - ); - } - log_construction_retry_start(attempt, attempt_seed, perturbation_seed); - - match Self::build_with_kernel_inner_seeded( - ::clone(kernel), - &shuffled, - topology_guarantee, - perturbation_seed, - true, - grid_cell_size, - batch_repair_policy, - use_global_repair_fallback, - ) { - Ok(candidate) => match candidate.is_delaunay_via_flips() { - Ok(()) => { - log_construction_retry_result( - attempt, - Some(attempt_seed), - perturbation_seed, - "succeeded", - None, - None, - ); - return Ok(candidate); - } - Err(err) => { - last_error = - format!("Delaunay property violated after construction: {err}"); - } - }, - Err(err) => { - let err_string = err.to_string(); - if Self::is_non_retryable_construction_error(&err) { - log_construction_retry_result( - attempt, - Some(attempt_seed), - perturbation_seed, - "failed", - Some(&err_string), - None, - ); - return Err(err); - } - last_error = err_string; + last_stats.replace(statistics); + last_error = error.to_string(); } } @@ -3753,7 +2539,7 @@ where attempt_seed, perturbation_seed, last_error = %last_error, - "build_with_shuffled_retries: attempt failed: {last_error}" + "build_with_shuffled_retries_with_construction_statistics: attempt failed: {last_error}" ); } log_construction_retry_result( @@ -3762,379 +2548,258 @@ where perturbation_seed, "failed", Some(&last_error), - None, + last_stats.as_ref(), ); } // Treat persistent construction failures or Delaunay violations as hard construction // errors so callers can deterministically reject. - Err(TriangulationConstructionError::GeometricDegeneracy { - message: format!( - "Delaunay construction failed after shuffled reconstruction attempts: {last_error}" - ), - } - .into()) + Err(DelaunayTriangulationConstructionErrorWithStatistics { + error: TriangulationConstructionError::GeometricDegeneracy { + message: format!( + "Delaunay construction failed after shuffled reconstruction attempts: {last_error}" + ), + } + .into(), + statistics: aggregate_stats, + }) } - /// Mirrors shuffled retry construction while preserving per-attempt - /// statistics for callers that need skip and retry diagnostics. - #[expect( - clippy::too_many_lines, - reason = "statistics variant mirrors construction retry flow for comparable diagnostics" - )] + /// Runs batch construction without statistics while preserving the same + /// final validation path as the statistics variant. + pub(crate) fn build_with_kernel_inner( + kernel: K, + vertices: &[Vertex], + topology_guarantee: TopologyGuarantee, + grid_cell_size: Option, + batch_repair_policy: DelaunayRepairPolicy, + use_global_repair_fallback: bool, + ) -> Result { + let dt = Self::build_with_kernel_inner_seeded( + kernel, + vertices, + topology_guarantee, + 0, + true, + grid_cell_size, + batch_repair_policy, + use_global_repair_fallback, + )?; + + // `DelaunayCheckPolicy::EndOnly`: always run a final global Delaunay validation pass after + // batch construction. + tracing::debug!("post-construction: starting Delaunay validation (build)"); + let delaunay_started = Instant::now(); + let delaunay_result = dt.is_valid(); + tracing::debug!( + elapsed = ?delaunay_started.elapsed(), + success = delaunay_result.is_ok(), + "post-construction: Delaunay validation (build) completed" + ); + delaunay_result.map_err(|err| TriangulationConstructionError::GeometricDegeneracy { + message: format!("Delaunay property violated after construction: {err}"), + })?; + + Ok(dt) + } + + /// Runs batch construction with aggregate statistics without changing the + /// construction algorithm itself. #[expect( clippy::result_large_err, reason = "Internal helper propagates public by-value construction-statistics error type" )] - #[expect( - clippy::too_many_arguments, - reason = "statistics retry helper mirrors the non-statistics construction path" - )] - fn build_with_shuffled_retries_with_construction_statistics( - kernel: &K, + pub(crate) fn build_with_kernel_inner_with_construction_statistics( + kernel: K, vertices: &[Vertex], topology_guarantee: TopologyGuarantee, - attempts: NonZeroUsize, - base_seed: Option, grid_cell_size: Option, batch_repair_policy: DelaunayRepairPolicy, use_global_repair_fallback: bool, ) -> Result<(Self, ConstructionStatistics), DelaunayTriangulationConstructionErrorWithStatistics> { - let base_seed = base_seed.unwrap_or_else(|| Self::construction_shuffle_seed(vertices)); + let (dt, mut stats) = Self::build_with_kernel_inner_seeded_with_construction_statistics( + kernel, + vertices, + topology_guarantee, + 0, + true, + grid_cell_size, + batch_repair_policy, + use_global_repair_fallback, + )?; - #[cfg(debug_assertions)] - let log_shuffle = env::var_os("DELAUNAY_DEBUG_SHUFFLE").is_some(); - - #[cfg(debug_assertions)] - if log_shuffle { - tracing::debug!( - base_seed, - attempts = attempts.get(), - vertex_count = vertices.len(), - "build_with_shuffled_retries_with_construction_statistics: starting" - ); + // `DelaunayCheckPolicy::EndOnly`: always run a final global Delaunay validation pass after + // batch construction. + tracing::debug!("post-construction: starting Delaunay validation (build stats)"); + let delaunay_started = Instant::now(); + let delaunay_result = dt.is_valid(); + let delaunay_elapsed = delaunay_started.elapsed(); + stats + .telemetry + .record_construction_final_delaunay_validation_timing(duration_nanos_saturating( + delaunay_elapsed, + )); + tracing::debug!( + elapsed = ?delaunay_elapsed, + success = delaunay_result.is_ok(), + "post-construction: Delaunay validation (build stats) completed" + ); + if let Err(err) = delaunay_result { + return Err(DelaunayTriangulationConstructionErrorWithStatistics { + error: TriangulationConstructionError::GeometricDegeneracy { + message: format!("Delaunay property violated after construction: {err}"), + } + .into(), + statistics: stats, + }); } - let mut last_stats: Option = None; - let mut aggregate_stats = ConstructionStatistics::default(); + Ok((dt, stats)) + } - // Attempt 0: original order, no extra perturbation salt. - log_construction_retry_result(0, None, 0_u64, "started", None, None); - let mut last_error: String = - match Self::build_with_kernel_inner_seeded_with_construction_statistics( - ::clone(kernel), - vertices, - topology_guarantee, - 0_u64, - true, - grid_cell_size, - batch_repair_policy, - use_global_repair_fallback, - ) { - Ok((candidate, mut stats)) => { - let delaunay_started = Instant::now(); - let delaunay_result = candidate.is_delaunay_via_flips(); - stats - .telemetry - .record_construction_final_delaunay_validation_timing( - duration_nanos_saturating(delaunay_started.elapsed()), - ); - match delaunay_result { - Ok(()) => { - aggregate_stats.merge_from(&stats); - log_construction_retry_result( - 0, - None, - 0_u64, - "succeeded", - None, - Some(&stats), - ); - return Ok((candidate, aggregate_stats)); - } - Err(err) => { - aggregate_stats.merge_from(&stats); - last_stats.replace(stats); - format!("Delaunay property violated after construction: {err}") - } - } - } - Err(err) => { - let DelaunayTriangulationConstructionErrorWithStatistics { error, statistics } = - err; - aggregate_stats.merge_from(&statistics); - if Self::is_non_retryable_construction_error(&error) { - let last_error = error.to_string(); - log_construction_retry_result( - 0, - None, - 0_u64, - "failed", - Some(&last_error), - Some(&statistics), - ); - return Err(DelaunayTriangulationConstructionErrorWithStatistics { - error, - statistics: aggregate_stats, - }); - } - last_stats.replace(statistics); - error.to_string() + /// Implements the seeded batch-construction core so retry and statistics + /// entry points share perturbation behavior. + #[expect( + clippy::result_large_err, + reason = "Internal helper propagates public by-value construction-statistics error type" + )] + #[expect( + clippy::too_many_arguments, + reason = "seeded construction helper carries retry, repair, and validation knobs" + )] + fn build_with_kernel_inner_seeded_with_construction_statistics( + kernel: K, + vertices: &[Vertex], + topology_guarantee: TopologyGuarantee, + perturbation_seed: u64, + run_final_repair: bool, + grid_cell_size: Option, + batch_repair_policy: DelaunayRepairPolicy, + use_global_repair_fallback: bool, + ) -> Result<(Self, ConstructionStatistics), DelaunayTriangulationConstructionErrorWithStatistics> + { + if vertices.len() < D + 1 { + return Err(DelaunayTriangulationConstructionErrorWithStatistics { + error: TriangulationConstructionError::InsufficientVertices { + dimension: D, + source: SimplexValidationError::InsufficientVertices { + actual: vertices.len(), + expected: D + 1, + dimension: D, + }, } - }; - - #[cfg(debug_assertions)] - if log_shuffle { - tracing::debug!( - attempt = 0, - perturbation_seed = 0_u64, - last_error = %last_error, - "build_with_shuffled_retries_with_construction_statistics: initial attempt failed: {last_error}" - ); + .into(), + statistics: ConstructionStatistics::default(), + }); } - log_construction_retry_result( - 0, - None, - 0_u64, - "failed", - Some(&last_error), - last_stats.as_ref(), - ); - // Shuffled retries (total iterations: attempts shuffled). - for attempt in 1..=attempts.get() { - let mut shuffled = vertices.to_vec(); + // Build initial simplex directly (no Bowyer-Watson) + let initial_vertices = &vertices[..=D]; + let tds = Triangulation::::build_initial_simplex(initial_vertices).map_err( + |error| DelaunayTriangulationConstructionErrorWithStatistics { + error: error.into(), + statistics: ConstructionStatistics::default(), + }, + )?; - let mut attempt_seed = - base_seed.wrapping_add((attempt as u64).wrapping_mul(DELAUNAY_SHUFFLE_SEED_SALT)); - if attempt_seed == 0 { - attempt_seed = 1; - } + let mut dt = Self { + tri: Triangulation { + kernel, + tds, + global_topology: GlobalTopology::DEFAULT, + validation_policy: topology_guarantee.default_validation_policy(), + topology_guarantee, + }, + insertion_state: DelaunayInsertionState::new(), + spatial_index: None, + }; - Self::shuffle_vertices(&mut shuffled, attempt_seed); + // During batch construction, use suspicion-driven validation instead of + // per-insertion validation. Running a full O(simplices) topology check after + // every insertion is prohibitively expensive at scale (O(n²) total). The + // OnSuspicion policy only validates when the insertion logic itself flags a + // potential issue (e.g. after rollback/retry). A comprehensive post- + // construction validation in finalize_bulk_construction catches any issues + // that slip through. + // + // Exception: PLManifoldStrict requires per-insertion vertex-link validation, + // so we must use ValidationPolicy::Always to satisfy that guarantee. + let original_validation_policy = dt.tri.validation_policy; + dt.tri.validation_policy = if dt + .tri + .topology_guarantee + .requires_vertex_links_during_insertion() + { + ValidationPolicy::Always + } else if dt.tri.topology_guarantee.requires_ridge_links() { + ValidationPolicy::OnSuspicion + } else { + ValidationPolicy::DebugOnly + }; - // Vary the deterministic perturbation pattern across retry attempts. - let perturbation_seed = attempt_seed ^ 0xD1B5_4A32_D192_ED03; + // Disable maybe_repair_after_insertion during bulk construction: its full pipeline + // (multi-pass repair + topology validation + heuristic rebuild) is too expensive + // per insertion. Instead, insert_remaining_vertices_seeded accumulates the local + // frontier touched by successful insertions and calls repair_delaunay_local_single_pass + // at the requested cadence (no topology check, no heuristic rebuild, soft-fail on + // non-convergence for D≥4). Soft-failed repair frontiers are retained for the final + // seeded repair in finalize_bulk_construction. + let original_repair_policy = dt.insertion_state.delaunay_repair_policy; + dt.insertion_state.delaunay_repair_policy = DelaunayRepairPolicy::Never; + dt.insertion_state.use_global_repair_fallback = use_global_repair_fallback; - #[cfg(debug_assertions)] - if log_shuffle { - tracing::debug!( - attempt, - attempt_seed, - perturbation_seed, - "build_with_shuffled_retries_with_construction_statistics: shuffled attempt starting" - ); - } - log_construction_retry_start(attempt, attempt_seed, perturbation_seed); + let mut stats = ConstructionStatistics::default(); + let simplex_stats = InsertionStatistics { + attempts: 1, + ..InsertionStatistics::default() + }; + for _ in 0..=D { + stats.record_insertion(&simplex_stats); + } - match Self::build_with_kernel_inner_seeded_with_construction_statistics( - ::clone(kernel), - &shuffled, - topology_guarantee, - perturbation_seed, - true, - grid_cell_size, - batch_repair_policy, - use_global_repair_fallback, - ) { - Ok((candidate, mut stats)) => { - let delaunay_started = Instant::now(); - let delaunay_result = candidate.is_delaunay_via_flips(); - stats - .telemetry - .record_construction_final_delaunay_validation_timing( - duration_nanos_saturating(delaunay_started.elapsed()), - ); - match delaunay_result { - Ok(()) => { - aggregate_stats.merge_from(&stats); - log_construction_retry_result( - attempt, - Some(attempt_seed), - perturbation_seed, - "succeeded", - None, - Some(&stats), - ); - return Ok((candidate, aggregate_stats)); - } - Err(err) => { - aggregate_stats.merge_from(&stats); - last_stats.replace(stats); - last_error = - format!("Delaunay property violated after construction: {err}"); - } - } - } - Err(err) => { - let DelaunayTriangulationConstructionErrorWithStatistics { error, statistics } = - err; - aggregate_stats.merge_from(&statistics); - if Self::is_non_retryable_construction_error(&error) { - let last_error = error.to_string(); - log_construction_retry_result( - attempt, - Some(attempt_seed), - perturbation_seed, - "failed", - Some(&last_error), - Some(&statistics), - ); - return Err(DelaunayTriangulationConstructionErrorWithStatistics { - error, - statistics: aggregate_stats, - }); - } - last_stats.replace(statistics); - last_error = error.to_string(); - } - } - - #[cfg(debug_assertions)] - if log_shuffle { - tracing::debug!( - attempt, - attempt_seed, - perturbation_seed, - last_error = %last_error, - "build_with_shuffled_retries_with_construction_statistics: attempt failed: {last_error}" - ); - } - log_construction_retry_result( - attempt, - Some(attempt_seed), - perturbation_seed, - "failed", - Some(&last_error), - last_stats.as_ref(), - ); - } - - // Treat persistent construction failures or Delaunay violations as hard construction - // errors so callers can deterministically reject. - Err(DelaunayTriangulationConstructionErrorWithStatistics { - error: TriangulationConstructionError::GeometricDegeneracy { - message: format!( - "Delaunay construction failed after shuffled reconstruction attempts: {last_error}" - ), - } - .into(), - statistics: aggregate_stats, - }) - } - - /// Avoids retry work when construction has no incremental phase to reorder. - const fn should_retry_construction(vertices: &[Vertex]) -> bool { - D >= 2 && vertices.len() > D + 1 - } - - /// Derives an input-order-independent seed so retries are reproducible for - /// the same vertex set. - fn construction_shuffle_seed(vertices: &[Vertex]) -> u64 { - let mut vertex_hashes = Vec::with_capacity(vertices.len()); - for vertex in vertices { - let mut hasher = FastHasher::default(); - vertex.hash(&mut hasher); - vertex_hashes.push(hasher.finish()); - } - vertex_hashes.sort_unstable(); - stable_hash_u64_slice(&vertex_hashes) - } - - /// Keeps construction retry shuffling deterministic for diagnostics and - /// tests. - fn shuffle_vertices(vertices: &mut [Vertex], seed: u64) { - let mut rng = StdRng::seed_from_u64(seed); - vertices.shuffle(&mut rng); - } - - /// Runs batch construction without statistics while preserving the same - /// final validation path as the statistics variant. - fn build_with_kernel_inner( - kernel: K, - vertices: &[Vertex], - topology_guarantee: TopologyGuarantee, - grid_cell_size: Option, - batch_repair_policy: DelaunayRepairPolicy, - use_global_repair_fallback: bool, - ) -> Result { - let dt = Self::build_with_kernel_inner_seeded( - kernel, + let mut soft_fail_seeds: Vec = Vec::new(); + let mut pending_repair_seeds: Vec = Vec::new(); + let insert_loop_started = Instant::now(); + let insert_result = dt.insert_remaining_vertices_seeded( vertices, - topology_guarantee, - 0, - true, + perturbation_seed, grid_cell_size, batch_repair_policy, - use_global_repair_fallback, - )?; - - // `DelaunayCheckPolicy::EndOnly`: always run a final global Delaunay validation pass after - // batch construction. - tracing::debug!("post-construction: starting Delaunay validation (build)"); - let delaunay_started = Instant::now(); - let delaunay_result = dt.is_valid(); - tracing::debug!( - elapsed = ?delaunay_started.elapsed(), - success = delaunay_result.is_ok(), - "post-construction: Delaunay validation (build) completed" + Some(&mut stats), + &mut pending_repair_seeds, + &mut soft_fail_seeds, ); - delaunay_result.map_err(|err| TriangulationConstructionError::GeometricDegeneracy { - message: format!("Delaunay property violated after construction: {err}"), - })?; - - Ok(dt) - } + stats + .telemetry + .record_construction_insert_loop_timing(duration_nanos_saturating( + insert_loop_started.elapsed(), + )); + if let Err(error) = insert_result { + return Err(DelaunayTriangulationConstructionErrorWithStatistics { + error, + statistics: stats, + }); + } - /// Runs batch construction with aggregate statistics without changing the - /// construction algorithm itself. - #[expect( - clippy::result_large_err, - reason = "Internal helper propagates public by-value construction-statistics error type" - )] - fn build_with_kernel_inner_with_construction_statistics( - kernel: K, - vertices: &[Vertex], - topology_guarantee: TopologyGuarantee, - grid_cell_size: Option, - batch_repair_policy: DelaunayRepairPolicy, - use_global_repair_fallback: bool, - ) -> Result<(Self, ConstructionStatistics), DelaunayTriangulationConstructionErrorWithStatistics> - { - let (dt, mut stats) = Self::build_with_kernel_inner_seeded_with_construction_statistics( - kernel, - vertices, - topology_guarantee, - 0, - true, - grid_cell_size, + let finalize_started = Instant::now(); + let finalize_result = dt.finalize_bulk_construction( + original_validation_policy, + original_repair_policy, + run_final_repair, batch_repair_policy, - use_global_repair_fallback, - )?; - - // `DelaunayCheckPolicy::EndOnly`: always run a final global Delaunay validation pass after - // batch construction. - tracing::debug!("post-construction: starting Delaunay validation (build stats)"); - let delaunay_started = Instant::now(); - let delaunay_result = dt.is_valid(); - let delaunay_elapsed = delaunay_started.elapsed(); + &pending_repair_seeds, + &soft_fail_seeds, + Some(&mut stats.telemetry), + ); stats .telemetry - .record_construction_final_delaunay_validation_timing(duration_nanos_saturating( - delaunay_elapsed, + .record_construction_finalize_timing(duration_nanos_saturating( + finalize_started.elapsed(), )); - tracing::debug!( - elapsed = ?delaunay_elapsed, - success = delaunay_result.is_ok(), - "post-construction: Delaunay validation (build stats) completed" - ); - if let Err(err) = delaunay_result { + if let Err(error) = finalize_result { return Err(DelaunayTriangulationConstructionErrorWithStatistics { - error: TriangulationConstructionError::GeometricDegeneracy { - message: format!("Delaunay property violated after construction: {err}"), - } - .into(), + error, statistics: stats, }); } @@ -4142,17 +2807,13 @@ where Ok((dt, stats)) } - /// Implements the seeded batch-construction core so retry and statistics - /// entry points share perturbation behavior. - #[expect( - clippy::result_large_err, - reason = "Internal helper propagates public by-value construction-statistics error type" - )] + /// Implements the non-statistics seeded construction core for callers that + /// only need the triangulation. #[expect( clippy::too_many_arguments, reason = "seeded construction helper carries retry, repair, and validation knobs" )] - fn build_with_kernel_inner_seeded_with_construction_statistics( + fn build_with_kernel_inner_seeded( kernel: K, vertices: &[Vertex], topology_guarantee: TopologyGuarantee, @@ -4161,31 +2822,22 @@ where grid_cell_size: Option, batch_repair_policy: DelaunayRepairPolicy, use_global_repair_fallback: bool, - ) -> Result<(Self, ConstructionStatistics), DelaunayTriangulationConstructionErrorWithStatistics> - { + ) -> Result { if vertices.len() < D + 1 { - return Err(DelaunayTriangulationConstructionErrorWithStatistics { - error: TriangulationConstructionError::InsufficientVertices { + return Err(TriangulationConstructionError::InsufficientVertices { + dimension: D, + source: SimplexValidationError::InsufficientVertices { + actual: vertices.len(), + expected: D + 1, dimension: D, - source: SimplexValidationError::InsufficientVertices { - actual: vertices.len(), - expected: D + 1, - dimension: D, - }, - } - .into(), - statistics: ConstructionStatistics::default(), - }); + }, + } + .into()); } // Build initial simplex directly (no Bowyer-Watson) let initial_vertices = &vertices[..=D]; - let tds = Triangulation::::build_initial_simplex(initial_vertices).map_err( - |error| DelaunayTriangulationConstructionErrorWithStatistics { - error: error.into(), - statistics: ConstructionStatistics::default(), - }, - )?; + let tds = Triangulation::::build_initial_simplex(initial_vertices)?; let mut dt = Self { tri: Triangulation { @@ -4200,12 +2852,8 @@ where }; // During batch construction, use suspicion-driven validation instead of - // per-insertion validation. Running a full O(simplices) topology check after - // every insertion is prohibitively expensive at scale (O(n²) total). The - // OnSuspicion policy only validates when the insertion logic itself flags a - // potential issue (e.g. after rollback/retry). A comprehensive post- - // construction validation in finalize_bulk_construction catches any issues - // that slip through. + // per-insertion validation (see _with_construction_statistics variant for + // rationale: O(n²) avoidance + post-construction catch-all). // // Exception: PLManifoldStrict requires per-insertion vertex-link validation, // so we must use ValidationPolicy::Always to satisfy that guarantee. @@ -4222,139 +2870,7 @@ where ValidationPolicy::DebugOnly }; - // Disable maybe_repair_after_insertion during bulk construction: its full pipeline - // (multi-pass repair + topology validation + heuristic rebuild) is too expensive - // per insertion. Instead, insert_remaining_vertices_seeded accumulates the local - // frontier touched by successful insertions and calls repair_delaunay_local_single_pass - // at the requested cadence (no topology check, no heuristic rebuild, soft-fail on - // non-convergence for D≥4). Soft-failed repair frontiers are retained for the final - // seeded repair in finalize_bulk_construction. - let original_repair_policy = dt.insertion_state.delaunay_repair_policy; - dt.insertion_state.delaunay_repair_policy = DelaunayRepairPolicy::Never; - dt.insertion_state.use_global_repair_fallback = use_global_repair_fallback; - - let mut stats = ConstructionStatistics::default(); - let simplex_stats = InsertionStatistics { - attempts: 1, - ..InsertionStatistics::default() - }; - for _ in 0..=D { - stats.record_insertion(&simplex_stats); - } - - let mut soft_fail_seeds: Vec = Vec::new(); - let mut pending_repair_seeds: Vec = Vec::new(); - let insert_loop_started = Instant::now(); - let insert_result = dt.insert_remaining_vertices_seeded( - vertices, - perturbation_seed, - grid_cell_size, - batch_repair_policy, - Some(&mut stats), - &mut pending_repair_seeds, - &mut soft_fail_seeds, - ); - stats - .telemetry - .record_construction_insert_loop_timing(duration_nanos_saturating( - insert_loop_started.elapsed(), - )); - if let Err(error) = insert_result { - return Err(DelaunayTriangulationConstructionErrorWithStatistics { - error, - statistics: stats, - }); - } - - let finalize_started = Instant::now(); - let finalize_result = dt.finalize_bulk_construction( - original_validation_policy, - original_repair_policy, - run_final_repair, - batch_repair_policy, - &pending_repair_seeds, - &soft_fail_seeds, - Some(&mut stats.telemetry), - ); - stats - .telemetry - .record_construction_finalize_timing(duration_nanos_saturating( - finalize_started.elapsed(), - )); - if let Err(error) = finalize_result { - return Err(DelaunayTriangulationConstructionErrorWithStatistics { - error, - statistics: stats, - }); - } - - Ok((dt, stats)) - } - - /// Implements the non-statistics seeded construction core for callers that - /// only need the triangulation. - #[expect( - clippy::too_many_arguments, - reason = "seeded construction helper carries retry, repair, and validation knobs" - )] - fn build_with_kernel_inner_seeded( - kernel: K, - vertices: &[Vertex], - topology_guarantee: TopologyGuarantee, - perturbation_seed: u64, - run_final_repair: bool, - grid_cell_size: Option, - batch_repair_policy: DelaunayRepairPolicy, - use_global_repair_fallback: bool, - ) -> Result { - if vertices.len() < D + 1 { - return Err(TriangulationConstructionError::InsufficientVertices { - dimension: D, - source: SimplexValidationError::InsufficientVertices { - actual: vertices.len(), - expected: D + 1, - dimension: D, - }, - } - .into()); - } - - // Build initial simplex directly (no Bowyer-Watson) - let initial_vertices = &vertices[..=D]; - let tds = Triangulation::::build_initial_simplex(initial_vertices)?; - - let mut dt = Self { - tri: Triangulation { - kernel, - tds, - global_topology: GlobalTopology::DEFAULT, - validation_policy: topology_guarantee.default_validation_policy(), - topology_guarantee, - }, - insertion_state: DelaunayInsertionState::new(), - spatial_index: None, - }; - - // During batch construction, use suspicion-driven validation instead of - // per-insertion validation (see _with_construction_statistics variant for - // rationale: O(n²) avoidance + post-construction catch-all). - // - // Exception: PLManifoldStrict requires per-insertion vertex-link validation, - // so we must use ValidationPolicy::Always to satisfy that guarantee. - let original_validation_policy = dt.tri.validation_policy; - dt.tri.validation_policy = if dt - .tri - .topology_guarantee - .requires_vertex_links_during_insertion() - { - ValidationPolicy::Always - } else if dt.tri.topology_guarantee.requires_ridge_links() { - ValidationPolicy::OnSuspicion - } else { - ValidationPolicy::DebugOnly - }; - - // See the _with_construction_statistics variant for the repair policy rationale. + // See the _with_construction_statistics variant for the repair policy rationale. let original_repair_policy = dt.insertion_state.delaunay_repair_policy; dt.insertion_state.delaunay_repair_policy = DelaunayRepairPolicy::Never; dt.insertion_state.use_global_repair_fallback = use_global_repair_fallback; @@ -4382,37 +2898,6 @@ where Ok(dt) } - /// Identifies D≥4 local-repair failures that can safely try escalation and - /// then enter the bounded soft-fail path. - const fn can_soft_fail(repair_err: &DelaunayRepairError) -> bool { - matches!( - repair_err, - DelaunayRepairError::NonConvergent { .. } - | DelaunayRepairError::PostconditionFailed { .. } - ) - } - - /// Converts non-soft-fail local-repair errors into construction failures so - /// the bulk loop does not canonicalize or keep mutating after unexpected - /// topology/flip failures. - fn map_hard_repair_error( - index: usize, - repair_err: DelaunayRepairError, - ) -> DelaunayTriangulationConstructionError { - let message = - format!("per-insertion Delaunay repair failed at index {index}: {repair_err}"); - if is_geometric_repair_error(&repair_err) { - TriangulationConstructionError::GeometricDegeneracy { message }.into() - } else { - DelaunayTriangulationConstructionError::Triangulation( - DelaunayConstructionFailure::DelaunayRepair { - phase: DelaunayConstructionRepairPhase::BatchLocal { index }, - source: Box::new(repair_err), - }, - ) - } - } - /// Records successful local-repair telemetry in one place so the repair loop /// stays focused on control flow. fn record_successful_local_repair_telemetry( @@ -5067,7 +3552,7 @@ where clippy::too_many_arguments, reason = "bulk finalization restores policies, repair state, and optional statistics telemetry" )] - fn finalize_bulk_construction( + pub(crate) fn finalize_bulk_construction( &mut self, original_validation_policy: ValidationPolicy, original_repair_policy: DelaunayRepairPolicy, @@ -5206,262 +3691,114 @@ where } } } +} - fn map_completion_repair_error( - message: String, - repair_error: DelaunayRepairError, - ) -> DelaunayTriangulationConstructionError { - if is_geometric_repair_error(&repair_error) { - TriangulationConstructionError::GeometricDegeneracy { message }.into() - } else { - DelaunayTriangulationConstructionError::Triangulation( - DelaunayConstructionFailure::DelaunayRepair { - phase: DelaunayConstructionRepairPhase::Completion, - source: Box::new(repair_error), - }, - ) - } - } - - /// Map an [`InsertionError`] from post-construction orientation canonicalization - /// into a [`TriangulationConstructionError`]. +impl DelaunayTriangulation +where + K: Kernel, + K::Scalar: NumCast, + U: DataType, + V: DataType, +{ + /// Creates an empty Delaunay triangulation with the given kernel. /// - /// Structural / data-structure errors (missing simplices, broken invariants) become - /// [`InternalInconsistency`](TriangulationConstructionError::InternalInconsistency) - /// because they indicate algorithmic bugs rather than bad input geometry. - /// Geometry-related failures (degenerate predicates, conflict regions, etc.) become - /// [`GeometricDegeneracy`](TriangulationConstructionError::GeometricDegeneracy). + /// Use this when a caller needs a custom kernel but wants to insert vertices + /// incrementally. /// - /// NOTE: This match is intentionally exhaustive over `InsertionError`. - /// When adding new variants, decide whether the failure mode is an internal - /// bug or an input-geometry problem. - fn map_orientation_canonicalization_error( - error: InsertionError, - ) -> TriangulationConstructionError { - match error { - // Geometric orientation errors (degenerate or negative) are - // geometry problems, not internal bugs. - InsertionError::TopologyValidation(error @ TdsError::Geometric(_)) => { - TriangulationConstructionError::GeometricDegeneracy { - message: format!( - "Failed to canonicalize orientation after post-construction repair: {error}" - ), - } - } - // Structural / data-structure errors indicate algorithmic bugs, - // not input-geometry problems. - // - // NOTE: OrientationViolation (coherent-orientation invariant breach between - // adjacent simplices) lands here rather than in the geometry arm above. After - // normalize_coherent_orientation() BFS, a surviving violation would mean the - // normalization algorithm failed its post-condition — an internal bug, not - // bad input geometry. DegenerateOrientation / NegativeOrientation capture - // the actual FP-related geometry failures. - error @ (InsertionError::TopologyValidation(_) - | InsertionError::TopologyValidationFailed { .. } - | InsertionError::CavityFilling { .. } - | InsertionError::NeighborWiring { .. } - | InsertionError::DuplicateUuid { .. }) => { - TriangulationConstructionError::InternalInconsistency { - message: format!( - "Failed to canonicalize orientation after post-construction repair: {error}" - ), - } - } - InsertionError::DelaunayRepairFailed { source, context } => { - let message = format!( - "Failed to canonicalize orientation after post-construction repair: \ - Delaunay repair failed ({context}): {source}" - ); - if is_geometric_repair_error(&source) { - TriangulationConstructionError::GeometricDegeneracy { message } - } else { - TriangulationConstructionError::InternalInconsistency { message } - } - } - // Geometry-related failures (degenerate input, predicate issues). - error @ (InsertionError::ConflictRegion(_) - | InsertionError::Location(_) - | InsertionError::NonManifoldTopology { .. } - | InsertionError::HullExtension { .. } - | InsertionError::DelaunayValidationFailed { .. } - | InsertionError::DuplicateCoordinates { .. }) => { - TriangulationConstructionError::GeometricDegeneracy { - message: format!( - "Failed to canonicalize orientation after post-construction repair: {error}" - ), - } - } + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::DelaunayTriangulation; + /// use delaunay::prelude::geometry::RobustKernel; + /// + /// let dt: DelaunayTriangulation, (), (), 3> = + /// DelaunayTriangulation::with_empty_kernel(RobustKernel::new()); + /// assert_eq!(dt.number_of_vertices(), 0); + /// ``` + #[must_use] + pub fn with_empty_kernel(kernel: K) -> Self { + let duplicate_tolerance = default_duplicate_tolerance::(); + + Self { + tri: Triangulation::new_empty(kernel), + insertion_state: DelaunayInsertionState::new(), + spatial_index: Some(HashGridIndex::new(duplicate_tolerance)), } } - /// Classifies insertion-layer failures as input degeneracy or internal - /// inconsistency for construction callers. - fn map_insertion_error(error: InsertionError) -> TriangulationConstructionError { - match error { - InsertionError::CavityFilling { reason } => { - TriangulationConstructionError::InsertionCavityFilling { source: reason } - } - InsertionError::NeighborWiring { reason } => { - TriangulationConstructionError::InternalInconsistency { - message: format!("Neighbor wiring failed: {reason}"), - } - } - InsertionError::TopologyValidation(source) => { - TriangulationConstructionError::from(TdsConstructionError::ValidationError(source)) - } - InsertionError::DuplicateUuid { entity, uuid } => { - TriangulationConstructionError::from(TdsConstructionError::DuplicateUuid { - entity, - uuid, - }) - } - InsertionError::DuplicateCoordinates { coordinates } => { - TriangulationConstructionError::DuplicateCoordinates { coordinates } - } - InsertionError::DelaunayRepairFailed { source, context } => { - let message = format!("Delaunay repair failed ({context}): {source}"); - if is_geometric_repair_error(&source) { - TriangulationConstructionError::GeometricDegeneracy { message } - } else { - TriangulationConstructionError::InternalInconsistency { message } - } - } - - InsertionError::ConflictRegion(source) => { - TriangulationConstructionError::InsertionConflictRegion { source } - } - InsertionError::Location(source) => { - TriangulationConstructionError::InsertionLocation { source } - } - InsertionError::NonManifoldTopology { - facet_hash, - simplex_count, - } => TriangulationConstructionError::InsertionNonManifoldTopology { - facet_hash, - simplex_count, - }, - InsertionError::HullExtension { reason } => { - TriangulationConstructionError::InsertionHullExtension { reason } - } - InsertionError::DelaunayValidationFailed { source } => { - TriangulationConstructionError::InsertionDelaunayValidation { source } - } - InsertionError::TopologyValidationFailed { message, source } => { - TriangulationConstructionError::InsertionTopologyValidation { message, source } - } - } - } -} - -// ============================================================================= -// QUERY, CONFIGURATION, TRAVERSAL, REPAIR & VALIDATION (Minimal Bounds) -// ============================================================================= -// -// Methods that only need `K: Kernel` — no scalar arithmetic. Downstream -// generic code (e.g. `delaunayize_by_flips`) does not need to carry -// `CoordinateScalar + NumCast` bounds when calling these methods. -// -// Follows the precedent of the existing PURE STRUCT ASSEMBLY impl block. - -impl DelaunayTriangulation -where - K: Kernel, -{ - // ------------------------------------------------------------------------- - // QUERY / ACCESSORS - // ------------------------------------------------------------------------- - - /// Returns the number of vertices in the triangulation. + /// Creates an empty Delaunay triangulation with a topology guarantee. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 0.0, 1.0]), - /// vertex!([0.2, 0.2, 0.2, 0.2]), - /// ]; + /// use delaunay::prelude::construction::{DelaunayTriangulation, TopologyGuarantee}; + /// use delaunay::prelude::geometry::RobustKernel; /// - /// let dt: DelaunayTriangulation<_, (), (), 4> = - /// DelaunayTriangulation::new(&vertices).unwrap(); - /// assert_eq!(dt.number_of_vertices(), 6); + /// let dt: DelaunayTriangulation, (), (), 3> = + /// DelaunayTriangulation::with_empty_kernel_and_topology_guarantee( + /// RobustKernel::new(), + /// TopologyGuarantee::PLManifold, + /// ); + /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); /// ``` #[must_use] - pub fn number_of_vertices(&self) -> usize { - self.tri.number_of_vertices() - } + pub fn with_empty_kernel_and_topology_guarantee( + kernel: K, + topology_guarantee: TopologyGuarantee, + ) -> Self { + let duplicate_tolerance = default_duplicate_tolerance::(); - /// Returns the number of simplices in the triangulation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 0.0, 1.0]), - /// ]; - /// - /// let dt: DelaunayTriangulation<_, (), (), 4> = - /// DelaunayTriangulation::new(&vertices).unwrap(); - /// // One 4-simplex in 4D - /// assert_eq!(dt.number_of_simplices(), 1); - /// ``` - #[must_use] - pub fn number_of_simplices(&self) -> usize { - self.tri.number_of_simplices() + let mut tri = Triangulation::new_empty(kernel); + tri.set_topology_guarantee(topology_guarantee); + Self { + tri, + insertion_state: DelaunayInsertionState::new(), + spatial_index: Some(HashGridIndex::new(duplicate_tolerance)), + } } - /// Returns the dimension of the triangulation. + /// Creates a Delaunay triangulation from vertices with an explicit kernel. /// - /// Returns the dimension `D` as an `i32`. + /// # Errors + /// Returns an error if construction fails or final validation fails. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::geometry::RobustKernel; /// /// let vertices = vec![ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 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: DelaunayTriangulation<_, (), (), 4> = - /// DelaunayTriangulation::new(&vertices).unwrap(); - /// assert_eq!(dt.dim(), 4); + /// let kernel = RobustKernel::::new(); + /// let dt: DelaunayTriangulation, (), (), 3> = + /// DelaunayTriangulation::with_kernel(&kernel, &vertices).unwrap(); + /// assert_eq!(dt.number_of_vertices(), 4); /// ``` - #[must_use] - pub fn dim(&self) -> i32 { - self.tri.dim() + pub fn with_kernel( + kernel: &K, + vertices: &[Vertex], + ) -> Result { + Self::with_topology_guarantee(kernel, vertices, TopologyGuarantee::DEFAULT) } - /// 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. + /// Creates a Delaunay triangulation with an explicit topology guarantee. /// - /// # Returns - /// - /// An iterator over `(SimplexKey, &Simplex)` pairs. + /// # Errors + /// Returns an error if construction fails or if the requested topology + /// guarantee cannot be satisfied. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::query::*; + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulation, TopologyGuarantee, vertex, + /// }; + /// use delaunay::prelude::geometry::RobustKernel; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -5469,31 +3806,42 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// - /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// for (simplex_key, simplex) in dt.simplices() { - /// println!("Simplex {:?} has {} vertices", simplex_key, simplex.number_of_vertices()); - /// } + /// let kernel = RobustKernel::::new(); + /// let dt: DelaunayTriangulation, (), (), 3> = + /// DelaunayTriangulation::with_topology_guarantee( + /// &kernel, + /// &vertices, + /// TopologyGuarantee::PLManifold, + /// ) + /// .unwrap(); + /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); /// ``` - pub fn simplices(&self) -> impl Iterator)> { - self.tri.tds.simplices() + pub fn with_topology_guarantee( + kernel: &K, + vertices: &[Vertex], + topology_guarantee: TopologyGuarantee, + ) -> Result { + Self::with_topology_guarantee_and_options( + kernel, + vertices, + topology_guarantee, + ConstructionOptions::default(), + ) } - /// 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. + /// Creates a Delaunay triangulation with topology and construction options. /// - /// # Returns - /// - /// An iterator over `(VertexKey, &Vertex)` pairs. + /// # Errors + /// Returns an error if construction fails, if validation fails, or if the + /// selected preprocessing options are invalid. /// /// # Examples /// /// ```rust - /// use delaunay::prelude::query::*; + /// use delaunay::prelude::construction::{ + /// ConstructionOptions, DelaunayTriangulation, TopologyGuarantee, vertex, + /// }; + /// use delaunay::prelude::geometry::RobustKernel; /// /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -5501,4207 +3849,850 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// - /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// for (vertex_key, vertex) in dt.vertices() { - /// println!("Vertex {:?} at {:?}", vertex_key, vertex.point()); - /// } + /// let kernel = RobustKernel::::new(); + /// let dt: DelaunayTriangulation, (), (), 3> = + /// DelaunayTriangulation::with_topology_guarantee_and_options( + /// &kernel, + /// &vertices, + /// TopologyGuarantee::PLManifold, + /// ConstructionOptions::default(), + /// ) + /// .unwrap(); + /// assert_eq!(dt.number_of_vertices(), 4); /// ``` - pub fn vertices(&self) -> impl Iterator)> { - self.tri.vertices() + pub fn with_topology_guarantee_and_options( + kernel: &K, + vertices: &[Vertex], + topology_guarantee: TopologyGuarantee, + options: ConstructionOptions, + ) -> Result { + let ConstructionOptions { + insertion_order, + dedup_policy, + initial_simplex, + retry_policy, + batch_repair_policy, + use_global_repair_fallback, + } = options; + + let preprocessed = Self::preprocess_vertices_for_construction( + vertices, + dedup_policy, + insertion_order, + initial_simplex, + )?; + let grid_cell_size = preprocessed.grid_cell_size(); + let primary_vertices: &[Vertex] = preprocessed.primary_slice(vertices); + let fallback_vertices = preprocessed.fallback_slice(); + + let build_with_vertices = |vertices: &[Vertex]| { + match retry_policy { + RetryPolicy::Disabled => {} + RetryPolicy::Shuffled { + attempts, + base_seed, + } => { + if Self::should_retry_construction(vertices) { + return Self::build_with_shuffled_retries( + kernel, + vertices, + topology_guarantee, + attempts, + base_seed, + grid_cell_size, + batch_repair_policy, + use_global_repair_fallback, + ); + } + } + RetryPolicy::DebugOnlyShuffled { + attempts, + base_seed, + } => { + if cfg!(any(test, debug_assertions)) + && Self::should_retry_construction(vertices) + { + return Self::build_with_shuffled_retries( + kernel, + vertices, + topology_guarantee, + attempts, + base_seed, + grid_cell_size, + batch_repair_policy, + use_global_repair_fallback, + ); + } + } + } + + Self::build_with_kernel_inner( + ::clone(kernel), + vertices, + topology_guarantee, + grid_cell_size, + batch_repair_policy, + use_global_repair_fallback, + ) + }; + + let result = build_with_vertices(primary_vertices); + if result.is_err() + && let Some(fallback) = fallback_vertices + { + return build_with_vertices(fallback); + } + + result } - /// 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. + /// Creates a Delaunay triangulation with construction statistics. /// - /// # Returns - /// - /// `None` if the key is not found. `Some(previous)` where `previous` is - /// the old `Option` value if the key exists. + /// # Errors + /// Returns [`DelaunayTriangulationConstructionErrorWithStatistics`] if + /// construction fails. The error includes the partial statistics collected + /// before failure. /// /// # Examples /// - /// ``` - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulationBuilder, Vertex, vertex, + /// ```rust + /// use delaunay::prelude::construction::{ + /// ConstructionOptions, DelaunayTriangulation, TopologyGuarantee, vertex, /// }; + /// use delaunay::prelude::geometry::RobustKernel; /// - /// let vertices: [Vertex; 3] = [ - /// vertex!([0.0, 0.0], 10i32), - /// vertex!([1.0, 0.0], 20), - /// vertex!([0.0, 1.0], 30), + /// 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 mut dt = DelaunayTriangulationBuilder::new(&vertices) - /// .build::<()>() + /// let kernel = RobustKernel::::new(); + /// let (dt, stats) = + /// DelaunayTriangulation::, (), (), 3>::with_options_and_statistics( + /// &kernel, + /// &vertices, + /// TopologyGuarantee::PLManifold, + /// ConstructionOptions::default(), + /// ) /// .unwrap(); - /// let key = dt.vertices().next().unwrap().0; - /// - /// 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(Some(99))); - /// assert_eq!(dt.tds().vertex(key).unwrap().data(), None); - /// ``` - #[inline] - pub fn set_vertex_data(&mut self, key: VertexKey, data: Option) -> Option> { - 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 - /// - /// `None` if the key is not found. `Some(previous)` where `previous` is - /// the old `Option` value if the key exists. - /// - /// # Examples - /// - /// ``` - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulationBuilder, vertex, - /// }; - /// - /// let vertices = [ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let mut dt = DelaunayTriangulationBuilder::new(&vertices) - /// .build::() - /// .unwrap(); - /// let key = dt.simplices().next().unwrap().0; - /// - /// let prev = dt.set_simplex_data(key, Some(42)); - /// assert_eq!(prev, Some(None)); - /// - /// // Clear data - /// let prev = dt.set_simplex_data(key, None); - /// assert_eq!(prev, Some(Some(42))); - /// assert_eq!(dt.tds().simplex(key).unwrap().data(), None); - /// ``` - #[inline] - pub fn set_simplex_data(&mut self, key: SimplexKey, data: Option) -> Option> { - self.tri.tds.set_simplex_data(key, data) - } - - /// Returns a reference to the underlying triangulation data structure. - /// - /// This provides access to the purely combinatorial Tds layer for - /// advanced operations and performance testing. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 0.0, 1.0]), - /// ]; - /// - /// let dt: DelaunayTriangulation<_, (), (), 4> = - /// DelaunayTriangulation::new(&vertices).unwrap(); - /// let tds = dt.tds(); - /// assert_eq!(tds.number_of_vertices(), 5); - /// ``` - #[must_use] - pub const fn tds(&self) -> &Tds { - &self.tri.tds - } - - /// Returns a mutable reference to the underlying triangulation data structure. - /// - /// This provides mutable access to the purely combinatorial Tds layer for - /// advanced operations and testing of internal algorithms. - /// - /// # Safety - /// - /// 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. - /// - /// 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::from_triangulation()`. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::ConvexHull; - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// let vertices: Vec<_> = 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::new(&vertices).unwrap(); - /// let hull = ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); - /// assert_eq!(hull.number_of_facets(), 4); - /// ``` - #[must_use] - pub const fn as_triangulation(&self) -> &Triangulation { - &self.tri - } - - /// Returns the insertion-time global topology validation policy used by the underlying - /// triangulation. - /// - /// This policy controls when Level 3 (`Triangulation::is_valid()`) is run automatically - /// during incremental insertion (as part of the topology safety net). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// - /// let dt: DelaunayTriangulation<_, (), (), 2> = - /// DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// assert_eq!( - /// dt.validation_policy(), - /// delaunay::prelude::triangulation::validation::ValidationPolicy::OnSuspicion - /// ); - /// ``` - #[inline] - #[must_use] - pub const fn validation_policy(&self) -> ValidationPolicy { - self.tri.validation_policy - } - - /// Sets 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()`.) - /// - /// If the requested policy is incompatible with the current topology guarantee (for example, - /// `ValidationPolicy::Never` with `TopologyGuarantee::PLManifold`), this runs - /// [`Triangulation::validate_at_completion`](crate::triangulation::Triangulation::validate_at_completion) - /// to provide immediate feedback and emits a warning. Call `validate_at_completion()` after - /// batch construction when using an incompatible combination. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// use delaunay::prelude::triangulation::validation::ValidationPolicy; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// - /// let mut dt: DelaunayTriangulation<_, (), (), 2> = - /// DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// 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); - } - /// Returns the automatic Delaunay repair policy. - #[inline] - #[must_use] - pub const fn delaunay_repair_policy(&self) -> DelaunayRepairPolicy { - self.insertion_state.delaunay_repair_policy - } - - /// Sets the automatic Delaunay repair policy. - #[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. - #[inline] - #[must_use] - pub const fn delaunay_check_policy(&self) -> DelaunayCheckPolicy { - self.insertion_state.delaunay_check_policy - } - - /// Sets the automatic global Delaunay validation policy. - #[inline] - pub const fn set_delaunay_check_policy(&mut self, policy: DelaunayCheckPolicy) { - self.insertion_state.delaunay_check_policy = policy; - } - - /// Runs flip-based Delaunay repair over the full triangulation. - /// - /// This is a manual entrypoint that performs a global scan of interior facets - /// and applies k=2/k=3 bistellar flips until locally Delaunay or until the flip - /// budget is exhausted. On success, geometric orientation is re-canonicalized - /// to the positive sign. - /// - /// # Errors - /// - /// Returns a [`DelaunayRepairError`] if the repair fails to converge, an underlying - /// flip operation fails, or post-repair orientation canonicalization fails. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// use delaunay::prelude::triangulation::repair::DelaunayRepairStats; - /// - /// 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 mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// let stats = dt.repair_delaunay_with_flips().unwrap(); - /// assert!(stats.facets_checked >= stats.flips_performed); + /// assert_eq!(dt.number_of_vertices(), stats.inserted); /// ``` - pub fn repair_delaunay_with_flips(&mut self) -> Result - where - K: ExactPredicates, - U: DataType, - V: DataType, - { - self.repair_delaunay_with_flips_capped(None) - } - - /// Runs flip-based repair with an optional per-attempt cap so public repair - /// and heuristic harnesses share one mutation path. - fn repair_delaunay_with_flips_capped( - &mut self, - max_flips: Option, - ) -> Result - where - K: ExactPredicates, - U: DataType, - V: DataType, - { - #[cfg(test)] - if test_hooks::force_repair_nonconvergent_enabled() { - return Err(test_hooks::synthetic_nonconvergent_error()); - } - let operation = TopologicalOperation::FacetFlip; - let topology = self.tri.topology_guarantee(); - if !operation.is_admissible_under(topology) { - return Err(DelaunayRepairError::InvalidTopology { - required: operation.required_topology(), - found: topology, - message: "Bistellar flips require a PL-manifold (vertex-link validation)", - }); - } - self.invalidate_locate_hint_cache(); - let (tds, kernel) = (&mut self.tri.tds, &self.tri.kernel); - let stats = repair_delaunay_with_flips_k2_k3(tds, kernel, None, topology, max_flips)?; - - // Re-canonicalize geometric orientation (#258): flip repair may leave - // the global sign negative. - self.ensure_positive_orientation()?; - - Ok(stats) - } - - /// Canonicalize geometric orientation to the positive sign, preserving - /// canonicalization failures as their own repair error variant. - fn ensure_positive_orientation(&mut self) -> Result<(), DelaunayRepairError> - where - U: DataType, - V: DataType, - { - self.tri - .normalize_and_promote_positive_orientation() - .map_err(|e| DelaunayRepairError::OrientationCanonicalizationFailed { - message: format!("after flip repair: {e}"), - }) - } - - /// Replays repair with an exact-predicate kernel before escalating to - /// heuristic rebuild. - fn repair_delaunay_with_flips_robust( - &mut self, - seed_simplices: Option<&[SimplexKey]>, - max_flips: Option, - ) -> Result - where - U: DataType, - V: DataType, - { - self.repair_delaunay_with_flips_robust_run(seed_simplices, max_flips) - .map(|run| run.stats) - } - - /// Replays repair with an exact-predicate kernel and returns the validation frontier. - fn repair_delaunay_with_flips_robust_run( - &mut self, - seed_simplices: Option<&[SimplexKey]>, - max_flips: Option, - ) -> Result - where - U: DataType, - V: DataType, - { - let topology = self.tri.topology_guarantee(); - let kernel = RobustKernel::::new(); - self.invalidate_locate_hint_cache(); - let (tds, kernel) = (&mut self.tri.tds, &kernel); - repair_delaunay_with_flips_k2_k3_run(tds, kernel, seed_simplices, topology, max_flips) - } - - /// Applies the repair policy only when the dimension and topology can - /// support bistellar flips. - fn should_run_delaunay_repair_for( - &self, - topology: TopologyGuarantee, - insertion_count: usize, - ) -> bool { - if D < 2 { - return false; - } - if self.tri.tds.number_of_simplices() == 0 { - return false; - } - - let policy = self.insertion_state.delaunay_repair_policy; - if policy == DelaunayRepairPolicy::Never { - return false; - } - - matches!( - policy.decide(insertion_count, topology, TopologicalOperation::FacetFlip), - RepairDecision::Proceed - ) - } - - /// Applies repair-policy and topology gates to non-insertion mutating operations. - /// - /// These operations do not have a meaningful insertion cadence, so every enabled - /// repair policy permits the post-mutation repair attempt. - fn should_run_delaunay_repair_after_mutation(&self, topology: TopologyGuarantee) -> bool { - if D < 2 { - return false; - } - if self.tri.tds.number_of_simplices() == 0 { - return false; - } - if self.insertion_state.delaunay_repair_policy == DelaunayRepairPolicy::Never { - return false; - } - - TopologicalOperation::FacetFlip.is_admissible_under(topology) - } - - /// Enables test-only repair fallback paths without exposing a public knob. - #[cfg_attr( - not(test), - expect( - clippy::missing_const_for_fn, - reason = "runtime feature and environment checks should remain ordinary functions" - ) + #[expect( + clippy::result_large_err, + reason = "Public API intentionally returns by-value construction statistics for compatibility" )] - fn force_heuristic_rebuild_enabled() -> bool { - #[cfg(test)] - { - test_hooks::force_heuristic_rebuild_enabled() - } - #[cfg(not(test))] - { - false - } - } -} - -// ============================================================================= -// ADVANCED REPAIR & HEURISTIC REBUILD (Requires Numeric Scalar Bounds) -// ============================================================================= -// -// `repair_delaunay_with_flips_advanced` can fall back to `rebuild_with_heuristic`, -// which constructs a new triangulation and therefore adds `NumCast` on top of -// the scalar requirements guaranteed by `Kernel`. - -impl DelaunayTriangulation -where - K: Kernel, - K::Scalar: NumCast, - U: DataType, - V: DataType, -{ - /// Runs flip-based Delaunay repair - /// - /// This first attempts the standard two-pass flip repair. If it fails to converge (or if - /// the result cannot be verified as Delaunay), it rebuilds the triangulation from the - /// current vertex set using a shuffled insertion order and a perturbation seed, then runs - /// a final flip-repair pass. On success, geometric orientation is re-canonicalized - /// to the positive sign. - /// - /// The returned outcome marks whether the heuristic fallback was used and records - /// the seeds needed to reproduce it (if desired). - /// - /// # Errors - /// - /// Returns [`DelaunayRepairError`] if the flip-based repair fails, the heuristic - /// rebuild fallback cannot construct a valid triangulation, or post-repair - /// orientation canonicalization fails. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// use delaunay::prelude::triangulation::repair::DelaunayRepairHeuristicConfig; - /// - /// 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 mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// let outcome = dt - /// .repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()) - /// .unwrap(); - /// assert!(outcome.stats.facets_checked >= outcome.stats.flips_performed); - /// ``` - pub fn repair_delaunay_with_flips_advanced( - &mut self, - config: DelaunayRepairHeuristicConfig, - ) -> Result - where - K: ExactPredicates, - { - if Self::force_heuristic_rebuild_enabled() { - let base_seed = self.heuristic_rebuild_base_seed(); - let seeds = config.resolve_seeds(base_seed); - let (candidate, stats, used_seeds) = - self.rebuild_with_heuristic(seeds, config.max_flips)?; - *self = candidate; - return Ok(DelaunayRepairOutcome { - stats, - heuristic: Some(used_seeds), - }); - } - let max_flips = config.max_flips; - match self.repair_delaunay_with_flips_capped(max_flips) { - Ok(stats) => Ok(DelaunayRepairOutcome { - stats, - heuristic: None, - }), - Err( - primary_err @ (DelaunayRepairError::NonConvergent { .. } - | DelaunayRepairError::PostconditionFailed { .. }), - ) => { - match self.repair_delaunay_with_flips_robust(None, max_flips) { - Ok(stats) => { - // Re-canonicalize geometric orientation (#258): robust flip - // repair may leave the global sign negative. - self.ensure_positive_orientation()?; - Ok(DelaunayRepairOutcome { - stats, - heuristic: None, - }) - } - Err(robust_err) => { - let base_seed = self.heuristic_rebuild_base_seed(); - let seeds = config.resolve_seeds(base_seed); - let (candidate, stats, used_seeds) = self - .rebuild_with_heuristic(seeds, max_flips) - .map_err(|heuristic_err| { - let heuristic_message = match heuristic_err { - DelaunayRepairError::HeuristicRebuildFailed { message } => { - message - } - other => other.to_string(), - }; - DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "primary repair failed ({primary_err}); robust fallback failed ({robust_err}); {heuristic_message}" - ), - } - })?; - *self = candidate; - Ok(DelaunayRepairOutcome { - stats, - heuristic: Some(used_seeds), - }) - } - } - } - Err(err) => Err(err), - } - } - - /// Rebuilds from the current vertex set with varied deterministic seeds when - /// flip repair cannot converge directly. #[expect( clippy::too_many_lines, - reason = "heuristic rebuild keeps point extraction, reconstruction, and validation together" + reason = "Statistics constructor handles preprocessing, retry, and fallback aggregation" )] - fn rebuild_with_heuristic( - &self, - base_seeds: DelaunayRepairHeuristicSeeds, - max_flips_override: Option, - ) -> Result<(Self, DelaunayRepairStats, DelaunayRepairHeuristicSeeds), DelaunayRepairError> - where - K: ExactPredicates, + pub fn with_options_and_statistics( + kernel: &K, + vertices: &[Vertex], + topology_guarantee: TopologyGuarantee, + options: ConstructionOptions, + ) -> Result<(Self, ConstructionStatistics), DelaunayTriangulationConstructionErrorWithStatistics> { - let base_vertices = self.collect_vertices_for_rebuild(); - - let mut last_error: Option = None; - - for attempt in 0..HEURISTIC_REBUILD_ATTEMPTS { - let seeds = if attempt == 0 { - base_seeds - } else { - // Vary the deterministic shuffle and perturbation patterns across attempts. - const SHUFFLE_SALT: u64 = 0x9E37_79B9_7F4A_7C15; - const PERTURB_SALT: u64 = 0xD1B5_4A32_D192_ED03; - - let attempt_u64 = attempt as u64; - - let mut shuffle_seed = base_seeds - .shuffle_seed - .wrapping_add(attempt_u64.wrapping_mul(SHUFFLE_SALT)); - if shuffle_seed == 0 { - shuffle_seed = 1; - } - - let mut perturbation_seed = - base_seeds.perturbation_seed ^ attempt_u64.wrapping_mul(PERTURB_SALT); - if perturbation_seed == 0 { - perturbation_seed = 1; - } - - DelaunayRepairHeuristicSeeds { - shuffle_seed, - perturbation_seed, - } - }; - - let rebuild_attempt = (|| { - let _guard = HeuristicRebuildRecursionGuard::enter(); - - // Shuffle vertices for this attempt. - let mut vertices = base_vertices.clone(); - let mut rng = rand::rngs::StdRng::seed_from_u64(seeds.shuffle_seed); - vertices.shuffle(&mut rng); - - // Heuristic rebuild is a last-resort fallback when global repair fails. Prefer an - // insertion schedule that keeps the triangulation near-Delaunay (local repairs on - // each insertion) so we do not get stuck in a non-regular configuration that flip - // repair cannot escape. - let topology_guarantee = self.tri.topology_guarantee(); - let global_topology = self.tri.global_topology(); - let mut candidate = Self::with_empty_kernel_and_topology_guarantee( - self.tri.kernel.clone(), - topology_guarantee, - ); - candidate.set_global_topology(global_topology); - - // During rebuild, force local repair after every insertion. We'll restore the caller's - // policies after we have a repaired candidate. - let rebuild_repair_policy = candidate.insertion_state.delaunay_repair_policy; - let rebuild_check_policy = candidate.insertion_state.delaunay_check_policy; - candidate.insertion_state.delaunay_repair_policy = - DelaunayRepairPolicy::EveryInsertion; - candidate.insertion_state.delaunay_check_policy = DelaunayCheckPolicy::EndOnly; - - for (idx, vertex) in vertices.into_iter().enumerate() { - let uuid = vertex.uuid(); - let coords = *vertex.point().coords(); - - let hint = candidate.insertion_state.last_inserted_simplex; - let insert_detail = { - let (tri, spatial_index) = - (&mut candidate.tri, &mut candidate.spatial_index); - tri.insert_with_statistics_seeded_indexed_detailed( - vertex, - None, - hint, - seeds.perturbation_seed, - spatial_index.as_mut(), - Some(idx), - ) - .map_err(|e| DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild insertion failed at idx={idx} uuid={uuid} coords={coords:?}: {e}" - ), - })? - }; - let repair_seed_simplices = insert_detail.repair_seed_simplices; - let delaunay_repair_required = insert_detail.delaunay_repair_required; - - match insert_detail.outcome { - InsertionOutcome::Inserted { vertex_key, hint } => { - candidate.insertion_state.last_inserted_simplex = hint; - candidate.insertion_state.delaunay_repair_insertion_count = candidate - .insertion_state - .delaunay_repair_insertion_count - .saturating_add(1); - - if delaunay_repair_required { - candidate - .maybe_repair_after_insertion_capped( - vertex_key, - hint, - &repair_seed_simplices, - max_flips_override, - ) - .map_err(|e| DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild repair failed at idx={idx} uuid={uuid} coords={coords:?}: {e}" - ), - })?; - } - - candidate - .maybe_check_after_insertion() - .map_err(|e| DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild Delaunay check failed at idx={idx} uuid={uuid} coords={coords:?}: {e}" - ), - })?; - } - InsertionOutcome::Skipped { error } => { - return Err(DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild skipped vertex at idx={idx} uuid={uuid} coords={coords:?}: {error}" - ), - }); - } - } - } - - candidate.tri.validation_policy = self.tri.validation_policy; - candidate.insertion_state.delaunay_repair_policy = - self.insertion_state.delaunay_repair_policy; - candidate.insertion_state.delaunay_check_policy = - self.insertion_state.delaunay_check_policy; - candidate.insertion_state.delaunay_repair_insertion_count = - self.insertion_state.delaunay_repair_insertion_count; - candidate.insertion_state.last_inserted_simplex = None; - - // Restore prior rebuild-only policies (kept for completeness; currently overwritten above). - let _ = (rebuild_repair_policy, rebuild_check_policy); - - let topology = candidate.tri.topology_guarantee(); - candidate.invalidate_locate_hint_cache(); - let (tds, kernel) = (&mut candidate.tri.tds, &candidate.tri.kernel); - let stats = repair_delaunay_with_flips_k2_k3( - tds, - kernel, - None, - topology, - max_flips_override, - )?; - - // Re-canonicalize geometric orientation (#258): the final flip - // repair may leave the global sign negative. - candidate.ensure_positive_orientation()?; - - Ok::<_, DelaunayRepairError>((candidate, stats)) - })(); - - match rebuild_attempt { - Ok((candidate, stats)) => return Ok((candidate, stats, seeds)), - Err(err) => { - last_error = Some(format!( - "attempt {}/{} (shuffle_seed={} perturbation_seed={}): {err}", - attempt + 1, - HEURISTIC_REBUILD_ATTEMPTS, - seeds.shuffle_seed, - seeds.perturbation_seed, - )); - } - } - } - - Err(DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "heuristic rebuild failed after {HEURISTIC_REBUILD_ATTEMPTS} attempts: {}", - last_error.unwrap_or_else(|| "unknown error".to_string()) - ), - }) - } - - /// Preserves vertex UUIDs and data so heuristic rebuilds remain an internal - /// repair strategy, not a user-visible remapping. - fn collect_vertices_for_rebuild(&self) -> Vec> { - self.tri - .tds - .vertices() - .map(|(_, vertex)| Vertex::new_with_uuid(*vertex.point(), vertex.uuid(), vertex.data)) - .collect() - } - - /// Derives rebuild seeds from the vertex set so fallback behavior is - /// reproducible regardless of slotmap iteration accidents. - fn heuristic_rebuild_base_seed(&self) -> u64 { - let mut vertex_hashes = Vec::with_capacity(self.tri.tds.number_of_vertices()); - for (_, vertex) in self.tri.tds.vertices() { - let mut hasher = FastHasher::default(); - vertex.hash(&mut hasher); - vertex_hashes.push(hasher.finish()); - } - vertex_hashes.sort_unstable(); - stable_hash_u64_slice(&vertex_hashes) - } -} - -// ============================================================================= -// CONFIGURATION & TRAVERSAL (Minimal Bounds, continued) -// ============================================================================= - -impl DelaunayTriangulation -where - K: Kernel, - U: DataType, - V: DataType, -{ - // ------------------------------------------------------------------------- - // CONFIGURATION - // ------------------------------------------------------------------------- - - /// Returns the topology guarantee used for Level 3 topology validation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulationBuilder, TopologyGuarantee, vertex, - /// }; - /// - /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); - /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); - /// ``` - #[inline] - #[must_use] - pub const fn topology_guarantee(&self) -> TopologyGuarantee { - self.tri.topology_guarantee() - } - - /// Returns runtime global topology metadata associated with this triangulation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulationBuilder, GlobalTopology, vertex, - /// }; - /// - /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); - /// assert!(dt.global_topology().is_euclidean()); - /// ``` - #[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::triangulation::construction::{ - /// DelaunayTriangulationBuilder, TopologyKind, vertex, - /// }; - /// - /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; - /// let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); - /// assert_eq!(dt.topology_kind(), TopologyKind::Euclidean); - /// ``` - #[inline] - #[must_use] - pub const fn topology_kind(&self) -> TopologyKind { - self.tri.topology_kind() - } - - /// Sets runtime global topology metadata on this triangulation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulationBuilder, GlobalTopology, vertex, - /// }; - /// - /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; - /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); - /// dt.set_global_topology(GlobalTopology::Euclidean); - /// assert!(dt.global_topology().is_euclidean()); - /// ``` - #[inline] - pub const fn set_global_topology(&mut self, global_topology: GlobalTopology) { - self.tri.set_global_topology(global_topology); - } - - /// Sets the topology guarantee used for Level 3 topology validation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulation, TopologyGuarantee, - /// }; - /// - /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); - /// dt.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - /// - /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); - /// ``` - #[inline] - pub fn set_topology_guarantee(&mut self, guarantee: TopologyGuarantee) { - self.tri.set_topology_guarantee(guarantee); - } - - /// 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 - /// - /// An iterator yielding `FacetView` objects for all facets. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// 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::new(&vertices).unwrap(); - /// - /// let facet_count = dt.facets().count(); - /// assert_eq!(facet_count, 4); // Tetrahedron has 4 facets - /// ``` - pub fn facets(&self) -> AllFacetsIter<'_, K::Scalar, U, V, D> { - self.tri.facets() - } - - /// Returns an iterator over boundary (hull) facets in the triangulation. - /// - /// Boundary facets are those that belong to exactly one simplex. This method - /// computes the facet-to-simplices map internally for convenience. - /// - /// # Returns - /// - /// An iterator yielding `FacetView` objects for boundary facets only. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// 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::new(&vertices).unwrap(); - /// - /// let boundary_count = dt.boundary_facets().count(); - /// assert_eq!(boundary_count, 4); // All facets are on boundary - /// ``` - pub fn boundary_facets(&self) -> BoundaryFacetsIter<'_, K::Scalar, U, V, D> { - self.tri.boundary_facets() - } - - /// Builds an immutable adjacency index for fast repeated topology queries. - /// - /// This is a convenience wrapper around - /// [`Triangulation::build_adjacency_index`](crate::triangulation::Triangulation::build_adjacency_index). - /// - /// # Errors - /// - /// Returns an error if the underlying triangulation data structure is internally inconsistent - /// (e.g., a simplex references a missing vertex key or a missing neighbor simplex key). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// // A single 3D tetrahedron has 6 unique edges. - /// 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::new(&vertices).unwrap(); - /// let index = dt.build_adjacency_index().unwrap(); - /// - /// assert_eq!(index.number_of_edges(), 6); - /// ``` - #[inline] - pub fn build_adjacency_index(&self) -> Result { - self.as_triangulation().build_adjacency_index() - } - - /// Returns an iterator over all unique edges in the triangulation. - /// - /// This is a convenience wrapper around - /// [`Triangulation::edges`](crate::triangulation::Triangulation::edges). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// // A single 3D tetrahedron has 6 unique edges. - /// 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::new(&vertices).unwrap(); - /// let edges: std::collections::HashSet<_> = dt.edges().collect(); - /// assert_eq!(edges.len(), 6); - /// ``` - pub fn edges(&self) -> impl Iterator + '_ { - self.as_triangulation().edges() - } - - /// Returns an iterator over all unique edges using a precomputed [`AdjacencyIndex`]. - /// - /// This avoids per-call deduplication and allocations. - /// - /// This is a convenience wrapper around - /// [`Triangulation::edges_with_index`](crate::triangulation::Triangulation::edges_with_index). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// 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::new(&vertices).unwrap(); - /// let index = dt.build_adjacency_index().unwrap(); - /// - /// let edges: std::collections::HashSet<_> = dt.edges_with_index(&index).collect(); - /// assert_eq!(edges.len(), 6); - /// ``` - pub fn edges_with_index<'a>( - &self, - index: &'a AdjacencyIndex, - ) -> impl Iterator + 'a { - self.as_triangulation().edges_with_index(index) - } - - /// Returns an iterator over all unique edges incident to a vertex. - /// - /// This is a convenience wrapper around - /// [`Triangulation::incident_edges`](crate::triangulation::Triangulation::incident_edges). - /// - /// If `v` is not present in this triangulation, the iterator is empty. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// 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::new(&vertices).unwrap(); - /// let v0 = dt.vertices().next().unwrap().0; - /// - /// // In a tetrahedron, each vertex has degree 3. - /// assert_eq!(dt.incident_edges(v0).count(), 3); - /// ``` - pub fn incident_edges(&self, v: VertexKey) -> impl Iterator + '_ { - self.as_triangulation().incident_edges(v) - } - - /// Returns an iterator over all unique edges incident to a vertex using a precomputed - /// [`AdjacencyIndex`]. - /// - /// If `v` is not present in the index, the iterator is empty. - /// - /// This is a convenience wrapper around - /// [`Triangulation::incident_edges_with_index`](crate::triangulation::Triangulation::incident_edges_with_index). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// 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::new(&vertices).unwrap(); - /// let index = dt.build_adjacency_index().unwrap(); - /// let v0 = dt.vertices().next().unwrap().0; - /// - /// assert_eq!(dt.incident_edges_with_index(&index, v0).count(), 3); - /// ``` - pub fn incident_edges_with_index<'a>( - &self, - index: &'a AdjacencyIndex, - v: VertexKey, - ) -> impl Iterator + 'a { - self.as_triangulation().incident_edges_with_index(index, v) - } - - /// Returns an iterator over all neighbors of a simplex. - /// - /// Boundary facets are omitted (only existing neighbors are yielded). If `c` is not - /// present, the iterator is empty. - /// - /// This is a convenience wrapper around - /// [`Triangulation::simplex_neighbors`](crate::triangulation::Triangulation::simplex_neighbors). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// // A single tetrahedron has no simplex neighbors. - /// 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::new(&vertices).unwrap(); - /// let simplex_key = dt.simplices().next().unwrap().0; - /// assert_eq!(dt.simplex_neighbors(simplex_key).count(), 0); - /// ``` - pub fn simplex_neighbors(&self, c: SimplexKey) -> impl Iterator + '_ { - self.as_triangulation().simplex_neighbors(c) - } - - /// Returns an iterator over all neighbors of a simplex using a precomputed [`AdjacencyIndex`]. - /// - /// If `c` is not present in the index, the iterator is empty. - /// - /// This is a convenience wrapper around - /// [`Triangulation::simplex_neighbors_with_index`](crate::triangulation::Triangulation::simplex_neighbors_with_index). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// // Two tetrahedra sharing a triangular facet => each tetra has exactly one neighbor. - /// let vertices: Vec<_> = vec![ - /// // Shared triangle - /// vertex!([0.0, 0.0, 0.0]), - /// vertex!([2.0, 0.0, 0.0]), - /// vertex!([1.0, 2.0, 0.0]), - /// // Two apices - /// vertex!([1.0, 0.7, 1.5]), - /// vertex!([1.0, 0.7, -1.5]), - /// ]; - /// - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); - /// let index = dt.build_adjacency_index().unwrap(); - /// - /// let simplex_key = dt.simplices().next().unwrap().0; - /// assert_eq!(dt.simplex_neighbors_with_index(&index, simplex_key).count(), 1); - /// ``` - pub fn simplex_neighbors_with_index<'a>( - &self, - index: &'a AdjacencyIndex, - c: SimplexKey, - ) -> impl Iterator + 'a { - self.as_triangulation() - .simplex_neighbors_with_index(index, c) - } - - /// Returns a slice view of a simplex's vertex keys. - /// - /// This is a zero-allocation accessor. If `c` is not present, returns `None`. - /// - /// This is a convenience wrapper around - /// [`Triangulation::simplex_vertices`](crate::triangulation::Triangulation::simplex_vertices). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// let simplex_key = dt.simplices().next().unwrap().0; - /// let simplex_vertices = dt.simplex_vertices(simplex_key).unwrap(); - /// assert_eq!(simplex_vertices.len(), 3); // D+1 for a 2D simplex - /// ``` - #[must_use] - pub fn simplex_vertices(&self, c: SimplexKey) -> Option<&[VertexKey]> { - self.as_triangulation().simplex_vertices(c) - } - - /// Returns a slice view of a vertex's coordinates. - /// - /// This is a zero-allocation accessor. If `v` is not present, returns `None`. - /// - /// This is a convenience wrapper around - /// [`Triangulation::vertex_coords`](crate::triangulation::Triangulation::vertex_coords). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// // Find the key for a known vertex by matching coordinates. - /// let v_key = dt - /// .vertices() - /// .find_map(|(vk, _)| (dt.vertex_coords(vk)? == [1.0, 0.0]).then_some(vk)) - /// .unwrap(); - /// - /// assert_eq!(dt.vertex_coords(v_key).unwrap(), [1.0, 0.0]); - /// ``` - #[must_use] - pub fn vertex_coords(&self, v: VertexKey) -> Option<&[K::Scalar]> { - self.as_triangulation().vertex_coords(v) - } -} - -// ============================================================================= -// MUTATION (Requires Numeric Scalar Bounds) -// ============================================================================= -// -// Incremental insertion, removal, and post-insertion repair/check helpers. -// These require `NumCast` for spatial-index construction, Triangulation-layer -// insertion, and Triangulation-layer removal. `Kernel` already guarantees -// `CoordinateScalar`. - -impl DelaunayTriangulation -where - K: Kernel, - K::Scalar: NumCast, - U: DataType, - V: DataType, -{ - /// Lazily seeds the spatial index from existing vertices so incremental - /// insertion can start from deserialized or manually constructed TDS state. - fn ensure_spatial_index_seeded(&mut self) { - if self.spatial_index.is_some() { - return; - } - - let duplicate_tolerance: K::Scalar = - ::from(1e-10_f64).unwrap_or_else(K::Scalar::default_tolerance); - let mut index: HashGridIndex = HashGridIndex::new(duplicate_tolerance); - - for (vkey, vertex) in self.tri.tds.vertices() { - index.insert_vertex(vkey, vertex.point().coords()); - } - - self.spatial_index = Some(index); - } - - /// Insert a vertex into the Delaunay triangulation using incremental cavity-based algorithm. - /// - /// This method handles all stages of triangulation construction: - /// - **Bootstrap (< D+1 vertices)**: Accumulates vertices without creating simplices - /// - **Initial simplex (D+1 vertices)**: Automatically builds the first D-simplex - /// - **Incremental (> D+1 vertices)**: Uses cavity-based insertion with point location - /// - /// # Algorithm - /// 1. Insert vertex into Tds - /// 2. Check vertex count: - /// - If < D+1: Return (bootstrap phase) - /// - If == D+1: Build initial simplex from all vertices - /// - If > D+1: Continue with steps 3-7 - /// 3. Locate simplex containing the point - /// 4. Find conflict region (simplices whose circumspheres contain the point) - /// 5. Extract cavity boundary - /// 6. Fill cavity (create new simplices) - /// 7. Wire neighbors locally - /// 8. Remove conflict simplices - /// - /// # Errors - /// Returns error if: - /// - Duplicate UUID detected - /// - Initial simplex construction fails (when reaching D+1 vertices) - /// - Point is on a facet, edge, or vertex (degenerate cases not yet implemented) - /// - Conflict region computation fails - /// - Cavity boundary extraction fails - /// - Cavity filling or neighbor wiring fails - /// - /// Note: Points outside the convex hull are handled automatically via hull extension. - /// - /// # Examples - /// - /// Incremental insertion from empty triangulation: - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// // Start with empty triangulation - /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); - /// assert_eq!(dt.number_of_vertices(), 0); - /// assert_eq!(dt.number_of_simplices(), 0); - /// - /// // Insert vertices one by one - bootstrap phase (no simplices yet) - /// dt.insert(vertex!([0.0, 0.0, 0.0])).unwrap(); - /// dt.insert(vertex!([1.0, 0.0, 0.0])).unwrap(); - /// dt.insert(vertex!([0.0, 1.0, 0.0])).unwrap(); - /// assert_eq!(dt.number_of_vertices(), 3); - /// assert_eq!(dt.number_of_simplices(), 0); // Still no simplices - /// - /// // 4th vertex triggers initial simplex creation - /// dt.insert(vertex!([0.0, 0.0, 1.0])).unwrap(); - /// assert_eq!(dt.number_of_vertices(), 4); - /// assert_eq!(dt.number_of_simplices(), 1); // First simplex created! - /// - /// // Further insertions use cavity-based algorithm - /// dt.insert(vertex!([0.2, 0.2, 0.2])).unwrap(); - /// assert_eq!(dt.number_of_vertices(), 5); - /// assert!(dt.number_of_simplices() > 1); - /// ``` - /// - /// Using batch construction (traditional approach): - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// // Create initial triangulation with 5 vertices (4-simplex) - /// let vertices = vec![ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 0.0, 1.0]), - /// ]; - /// let mut dt: DelaunayTriangulation<_, (), (), 4> = - /// DelaunayTriangulation::new(&vertices).unwrap(); - /// assert_eq!(dt.number_of_vertices(), 5); - /// - /// // Insert additional interior vertex - /// dt.insert(vertex!([0.2, 0.2, 0.2, 0.2])).unwrap(); - /// assert_eq!(dt.number_of_vertices(), 6); - /// assert!(dt.number_of_simplices() > 1); - /// ``` - pub fn insert(&mut self, vertex: Vertex) -> Result { - self.ensure_spatial_index_seeded(); - - // Fully delegate to Triangulation layer - // Triangulation handles: - // - Manifold maintenance (conflict simplices, cavity, repairs) - // - Bootstrap and initial simplex - // - Location and conflict region computation - // - // DelaunayTriangulation adds: - // - Kernel (provides in-sphere predicate for Delaunay property) - // - Hint caching for performance - // - Future: Delaunay property restoration after removal - // - // Transactional guard: post-steps (flip repair and/or global Delaunay checks) can fail. - // If they do, rollback to leave the triangulation unchanged. - let next_insertion_count = self - .insertion_state - .delaunay_repair_insertion_count - .saturating_add(1); - let could_have_simplices_after_insertion = self.tri.tds.number_of_simplices() > 0 - || self.tri.tds.number_of_vertices().saturating_add(1) > D; - let snapshot_needed = could_have_simplices_after_insertion - && (self.insertion_state.delaunay_repair_policy != DelaunayRepairPolicy::Never - || self - .insertion_state - .delaunay_check_policy - .should_check(next_insertion_count)); - let snapshot = - snapshot_needed.then(|| (self.tri.tds.clone_for_rollback(), self.insertion_state)); - - let insertion_result = (|| { - let hint = self.insertion_state.last_inserted_simplex; - let insert_detail = { - let (tri, spatial_index) = (&mut self.tri, &mut self.spatial_index); - tri.insert_with_statistics_seeded_indexed_detailed( - vertex, - None, - hint, - 0, - spatial_index.as_mut(), - None, - )? - }; - let repair_seed_simplices = insert_detail.repair_seed_simplices; - let delaunay_repair_required = insert_detail.delaunay_repair_required; - - match insert_detail.outcome { - InsertionOutcome::Inserted { - vertex_key: v_key, - hint, - } => { - self.insertion_state.last_inserted_simplex = hint; - self.insertion_state.delaunay_repair_insertion_count = self - .insertion_state - .delaunay_repair_insertion_count - .saturating_add(1); - if delaunay_repair_required { - self.maybe_repair_after_insertion(v_key, hint, &repair_seed_simplices)?; - } - self.maybe_check_after_insertion()?; - Ok(v_key) - } - InsertionOutcome::Skipped { error } => Err(error), - } - })(); - - match insertion_result { - Ok(v_key) => Ok(v_key), - Err(err) => { - if let Some((tds, insertion_state)) = snapshot { - self.spatial_index = None; - self.tri.tds = tds; - self.insertion_state = insertion_state; - } - Err(err) - } - } - } - - /// Insert a vertex and return the insertion outcome plus statistics. - /// - /// This is a convenience wrapper around the triangulation-layer insertion-with-statistics - /// implementation that also updates the internal `insertion_state.last_inserted_simplex` hint cache. - /// - /// # Errors - /// - /// Returns `Err(InsertionError)` only for non-retryable structural failures. - /// Retryable geometric degeneracies that exhaust all attempts return - /// `Ok((InsertionOutcome::Skipped { .. }, stats))`. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// use delaunay::prelude::triangulation::insertion::InsertionOutcome; - /// - /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); - /// - /// let (outcome, stats) = dt - /// .insert_with_statistics(vertex!([0.0, 0.0, 0.0])) - /// .unwrap(); - /// - /// assert!(stats.success()); - /// assert!(matches!(outcome, InsertionOutcome::Inserted { .. })); - /// ``` - pub fn insert_with_statistics( - &mut self, - vertex: Vertex, - ) -> Result<(InsertionOutcome, InsertionStatistics), InsertionError> { - self.ensure_spatial_index_seeded(); - - // Transactional guard: post-steps (flip repair and/or global Delaunay checks) can fail. - // If they do, rollback to leave the triangulation unchanged. - let next_insertion_count = self - .insertion_state - .delaunay_repair_insertion_count - .saturating_add(1); - let could_have_simplices_after_insertion = self.tri.tds.number_of_simplices() > 0 - || self.tri.tds.number_of_vertices().saturating_add(1) > D; - let snapshot_needed = could_have_simplices_after_insertion - && (self.insertion_state.delaunay_repair_policy != DelaunayRepairPolicy::Never - || self - .insertion_state - .delaunay_check_policy - .should_check(next_insertion_count)); - let snapshot = - snapshot_needed.then(|| (self.tri.tds.clone_for_rollback(), self.insertion_state)); - - let insertion_result = (|| { - let hint = self.insertion_state.last_inserted_simplex; - let insert_detail = { - let (tri, spatial_index) = (&mut self.tri, &mut self.spatial_index); - tri.insert_with_statistics_seeded_indexed_detailed( - vertex, - None, - hint, - 0, - spatial_index.as_mut(), - None, - )? - }; - let stats = insert_detail.stats; - let repair_seed_simplices = insert_detail.repair_seed_simplices; - let delaunay_repair_required = insert_detail.delaunay_repair_required; - - let outcome = match insert_detail.outcome { - InsertionOutcome::Inserted { vertex_key, hint } => { - self.insertion_state.last_inserted_simplex = hint; - self.insertion_state.delaunay_repair_insertion_count = self - .insertion_state - .delaunay_repair_insertion_count - .saturating_add(1); - if delaunay_repair_required { - self.maybe_repair_after_insertion( - vertex_key, - hint, - &repair_seed_simplices, - )?; - } - self.maybe_check_after_insertion()?; - InsertionOutcome::Inserted { vertex_key, hint } - } - other @ InsertionOutcome::Skipped { .. } => other, - }; - - Ok((outcome, stats)) - })(); - - match insertion_result { - Ok((outcome, stats)) => Ok((outcome, stats)), - Err(err) => { - if let Some((tds, insertion_state)) = snapshot { - self.spatial_index = None; - self.tri.tds = tds; - self.insertion_state = insertion_state; - } - Err(err) - } - } - } - - /// Keeps the default insertion path on the same repair helper as capped - /// debug and heuristic paths. - fn maybe_repair_after_insertion( - &mut self, - vertex_key: VertexKey, - hint: Option, - extra_seed_simplices: &[SimplexKey], - ) -> Result<(), InsertionError> { - self.maybe_repair_after_insertion_capped(vertex_key, hint, extra_seed_simplices, None) - } - - /// Like [`maybe_repair_after_insertion`](Self::maybe_repair_after_insertion) but - /// forwards an optional per-attempt flip cap to the underlying repair functions. - /// - /// `extra_seed_simplices` widens the local repair frontier beyond the inserted vertex - /// star. This is used when cavity reduction shrinks simplices out of the conflict - /// region: those simplices stay in the triangulation and may still need a local - /// Delaunay revisit even though they are no longer adjacent to the new vertex. - fn maybe_repair_after_insertion_capped( - &mut self, - vertex_key: VertexKey, - hint: Option, - extra_seed_simplices: &[SimplexKey], - max_flips: Option, - ) -> Result<(), InsertionError> { - let topology = self.tri.topology_guarantee(); - if !self.should_run_delaunay_repair_for( - topology, - self.insertion_state.delaunay_repair_insertion_count, - ) { - return Ok(()); - } - - // Prefer the merged local frontier when we have one; otherwise fall back to the - // validated locate hint so repair can still start from the inserted star. - let seed_simplices = - self.collect_local_repair_seed_simplices(vertex_key, extra_seed_simplices); - let hint_seed = hint.and_then(|ck| { - if !self.tri.tds.contains_simplex(ck) { - if env::var_os("DELAUNAY_REPAIR_TRACE").is_some() { - tracing::debug!( - "[repair] insertion seed hint missing (simplex={ck:?}, vertex={vertex_key:?})" - ); - } - return None; - } - - let contains_vertex = self - .tri - .tds - .simplex(ck) - .is_some_and(|simplex| simplex.contains_vertex(vertex_key)); - if !contains_vertex && env::var_os("DELAUNAY_REPAIR_TRACE").is_some() { - tracing::debug!( - "[repair] insertion seed hint does not contain vertex (simplex={ck:?}, vertex={vertex_key:?})" - ); - } - - contains_vertex.then_some(ck) - }); - - let seed_ref = if seed_simplices.is_empty() { - hint_seed.as_ref().map(std::slice::from_ref) - } else { - Some(seed_simplices.as_slice()) - }; - - let repair_result = { - self.invalidate_locate_hint_cache(); - let (tds, kernel) = (&mut self.tri.tds, &self.tri.kernel); - repair_delaunay_with_flips_k2_k3_run(tds, kernel, seed_ref, topology, max_flips) - }; - - #[cfg(test)] - let repair_result = if test_hooks::force_repair_nonconvergent_enabled() { - Err(test_hooks::synthetic_nonconvergent_error()) - } else { - repair_result - }; - - match repair_result { - Ok(run) => { - self.validate_ridge_links_after_repair(topology, &run)?; - } - Err( - e @ (DelaunayRepairError::NonConvergent { .. } - | DelaunayRepairError::PostconditionFailed { .. }), - ) => { - // Robust fallback: retry with `RobustKernel` which guarantees exact - // predicate evaluation. This covers 99.9%+ of repair failures. - // - // If the robust pass also fails, return an error. Callers that need - // the full heuristic rebuild (shuffled re-insertion) can invoke - // `repair_delaunay_with_flips_advanced()` explicitly. - let robust_run = self - .repair_delaunay_with_flips_robust_run(seed_ref, max_flips) - .map_err(|robust_err| InsertionError::DelaunayRepairFailed { - source: Box::new(robust_err), - context: DelaunayRepairFailureContext::LocalRepairRobustFallback { - initial: DelaunayRepairErrorSummary::from(&e), - }, - })?; - self.validate_ridge_links_after_repair(topology, &robust_run)?; - } - Err(e) => { - return Err(InsertionError::DelaunayRepairFailed { - source: Box::new(e), - context: DelaunayRepairFailureContext::LocalRepairNonRecoverable, - }); - } - } - - // Flip-based repair mutates simplex orderings; restore canonical positive geometric - // orientation before exposing the updated triangulation state. - self.tri.normalize_and_promote_positive_orientation()?; - self.tri - .validate_geometric_simplex_orientation() - .map_err(InsertionError::TopologyValidation)?; - Ok(()) - } - - /// Validates PL ridge links after a repair pass that actually performed flips. - /// - /// Ridge-link topology only changes where flips created replacement simplices, - /// so validation follows that mutation frontier even if the repair queues - /// were seeded from the full triangulation. If a repair reports flips - /// without a mutation frontier, fall back to a full simplex list defensively. - fn validate_ridge_links_after_repair( - &self, - topology: TopologyGuarantee, - run: &DelaunayRepairRun, - ) -> Result<(), InsertionError> { - if !topology.requires_ridge_links() || run.stats.flips_performed == 0 { - return Ok(()); - } - - let validate_simplices = |simplices: &[SimplexKey]| { - if simplices.is_empty() { - return Ok(()); - } - validate_ridge_links_for_simplices(&self.tri.tds, simplices) - .map_err(ridge_link_repair_validation_error) - }; - - if !run.touched_simplices.is_empty() { - if run.used_full_reseed && env::var_os("DELAUNAY_REPAIR_TRACE").is_some() { - tracing::debug!( - "[repair] validating ridge links on {} flip-created simplices after full reseed", - run.touched_simplices.len() - ); - } - return validate_simplices(&run.touched_simplices); - } - - let validation_simplices: Vec = self.tri.tds.simplex_keys().collect(); - validate_simplices(&validation_simplices) - } - - /// Merge the inserted vertex star with any simplices that cavity reduction touched and - /// left in place. Stale simplices are ignored so callers can pass raw cavity-trace sets. - fn collect_local_repair_seed_simplices( - &self, - vertex_key: VertexKey, - extra_seed_simplices: &[SimplexKey], - ) -> Vec { - let mut seen: FastHashSet = FastHashSet::default(); - let mut seed_simplices = Vec::new(); - - // Keep the inserted vertex star first because it is the hottest local region and - // the best chance of fixing ordinary post-insertion violations cheaply. - for simplex_key in self.tri.adjacent_simplices(vertex_key) { - if seen.insert(simplex_key) { - seed_simplices.push(simplex_key); - } - } - - // Then widen the frontier with simplices touched by cavity shaping that survived in - // the triangulation; deduping here lets callers pass raw trace buffers safely. - for &simplex_key in extra_seed_simplices { - if self.tri.tds.contains_simplex(simplex_key) && seen.insert(simplex_key) { - seed_simplices.push(simplex_key); - } - } - - seed_simplices - } - - /// Runs policy-controlled global validation after insertion so expensive - /// Delaunay checks stay opt-in for incremental workflows. - fn maybe_check_after_insertion(&self) -> Result<(), InsertionError> { - if self.tri.tds.number_of_simplices() == 0 { - return Ok(()); - } - - let policy = self.insertion_state.delaunay_check_policy; - let insertion_count = self.insertion_state.delaunay_repair_insertion_count; - if !policy.should_check(insertion_count) { - return Ok(()); - } - - self.is_valid() - .map_err(|e| InsertionError::DelaunayValidationFailed { source: e }) - } - - /// Removes a vertex and retriangulates the resulting cavity using fan triangulation. - /// - /// This operation delegates to `Triangulation::remove_vertex()` which: - /// 1. Finds all simplices containing the vertex - /// 2. Removes those simplices (creating a cavity) - /// 3. Fills the cavity with fan triangulation - /// 4. Wires neighbors and rebuilds vertex-simplex incidence - /// 5. Removes the vertex - /// - /// Fast-path: if the vertex star is a simplex (exactly D+1 incident simplices with - /// consistent adjacency), this method collapses it via the **inverse k=1** bistellar - /// flip. Otherwise it falls back to fan triangulation. - /// - /// The triangulation remains topologically valid after removal. However, both the - /// inverse k=1 fast-path and fan triangulation may temporarily violate the Delaunay - /// property in some cases. If the [`DelaunayRepairPolicy`] allows it, a flip-based - /// repair pass is run automatically after removal. - /// - /// The post-removal repair and orientation canonicalization steps are - /// transactional: if either step fails, this method restores the triangulation - /// and insertion state to their pre-removal state before returning the error. - /// The spatial index is retained across rollback because its keys are - /// validated against the live TDS before use. On successful removal, - /// topology-dependent locate hints are invalidated and the removed vertex key - /// is pruned from the spatial index. - /// - /// **Future Enhancement**: Delaunay-aware cavity retriangulation will be added for - /// removals. For now, occasional Delaunay violations after removal are expected and - /// can be addressed by running flip-based repair (e.g., [`repair_delaunay_with_flips`](Self::repair_delaunay_with_flips)) - /// or by leaving automatic repair enabled via [`DelaunayRepairPolicy`]. - /// - /// # Arguments - /// - /// * `vertex_key` - Key of the vertex to remove - /// - /// # Returns - /// - /// The number of simplices that were removed along with the vertex. Returns `Ok(0)` if - /// `vertex_key` does not refer to a vertex in the triangulation (e.g. a stale key from - /// a previously removed vertex or a key that was never inserted). This is a successful - /// no-op, not an error. - /// - /// # Errors - /// - /// Returns [`InvariantError`] if: - /// - The inverse k=1 flip encounters a neighbor-wiring failure (`InvariantError::Tds`). - /// - Fan retriangulation fails (`InvariantError::Tds`). - /// - Delaunay flip-based repair fails after removal - /// (`InvariantError::Delaunay(DelaunayTriangulationValidationError::RepairOperationFailed { .. })`). - /// - Orientation canonicalization fails after repair (`InvariantError::Tds`). - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// let interior = vertex!([0.3, 0.3]); - /// let interior_uuid = interior.uuid(); - /// let vertices = [ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// interior, - /// ]; - /// let mut dt = DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// // Find the key of a known interior vertex. - /// let vertex_key = dt - /// .vertices() - /// .find(|(_, v)| v.uuid() == interior_uuid) - /// .map(|(k, _)| k) - /// .unwrap(); - /// - /// // Remove the vertex and all simplices containing it - /// let simplices_removed = dt.remove_vertex(vertex_key).unwrap(); - /// println!("Removed {} simplices along with the vertex", simplices_removed); - /// - /// // Vertex removal preserves topology; automatic repair is attempted when enabled. - /// assert!(dt.as_triangulation().validate().is_ok()); - /// ``` - pub fn remove_vertex(&mut self, vertex_key: VertexKey) -> Result { - let Some(removed_vertex) = self.tri.tds.vertex(vertex_key) else { - return Ok(0); - }; - let removed_vertex_coords = *removed_vertex.point().coords(); - let snapshot = (self.tri.tds.clone_for_rollback(), self.insertion_state); - - let result = (|| { - // 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 self.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: Box::new(reason), - }, - } - .into()); - } - Err(_) => self.tri.remove_vertex(vertex_key)?, - }; - - let topology = self.tri.topology_guarantee(); - if self.should_run_delaunay_repair_after_mutation(topology) { - let seed_ref = seed_simplices.as_deref(); - let repair_result = { - self.invalidate_locate_hint_cache(); - let (tds, kernel) = (&mut self.tri.tds, &self.tri.kernel); - repair_delaunay_with_flips_k2_k3(tds, kernel, seed_ref, topology, None) - }; - - #[cfg(test)] - let repair_result = if test_hooks::force_repair_nonconvergent_enabled() { - Err(test_hooks::synthetic_nonconvergent_error()) - } else { - repair_result - }; - - repair_result.map_err(|source| { - InvariantError::Delaunay( - DelaunayTriangulationValidationError::RepairOperationFailed { - operation: DelaunayRepairOperation::VertexRemoval, - source: Box::new(source), - }, - ) - })?; - - // Re-canonicalize geometric orientation (#258): flip repair may leave - // the global sign negative. - self.tri - .normalize_and_promote_positive_orientation() - .map_err(|e| { - insertion_error_to_invariant_error( - e, - "Orientation canonicalization failed after vertex removal", - ) - })?; - } - - Ok(simplices_removed) - })(); - - match result { - Ok(simplices_removed) => { - self.insertion_state.last_inserted_simplex = None; - if let Some(index) = self.spatial_index.as_mut() { - index.remove_vertex(&vertex_key, &removed_vertex_coords); - } - Ok(simplices_removed) - } - Err(err) => { - let (tds, insertion_state) = snapshot; - self.tri.tds = tds; - self.insertion_state = insertion_state; - Err(err) - } - } - } -} - -// ============================================================================= -// VALIDATION (Minimal Bounds) -// ============================================================================= - -impl DelaunayTriangulation -where - K: Kernel, - U: DataType, - V: DataType, -{ - // ------------------------------------------------------------------------- - // VALIDATION - // ------------------------------------------------------------------------- - - /// Validates the Delaunay empty-circumsphere property (Level 4). - /// - /// This is the Delaunay layer's `is_valid`: it checks **only** the Delaunay property - /// and intentionally does **not** run lower-layer validation. - /// - /// **Performance**: Uses fast O(simplices) flip-based verification instead of the naive - /// O(simplices × vertices) brute-force check, providing ~40-100x speedup. This method is - /// correct for all properly-constructed triangulations (which is the standard case). - /// - /// For cumulative validation across the whole hierarchy, use [`validate`](Self::validate). - /// - /// # Errors - /// - /// Returns a [`DelaunayTriangulationValidationError`] if the empty-circumsphere test fails, or if - /// the underlying triangulation state is inconsistent and prevents geometric predicates - /// from being evaluated. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// let vertices_4d = [ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 4> = - /// DelaunayTriangulation::new(&vertices_4d).unwrap(); - /// - /// // Level 4: Delaunay property only - /// assert!(dt.is_valid().is_ok()); - /// ``` - pub fn is_valid(&self) -> Result<(), DelaunayTriangulationValidationError> { - // Use fast flip-based verification (O(simplices) instead of O(simplices × vertices)) - self.is_delaunay_via_flips().map_err(|err| { - DelaunayTriangulationValidationError::VerificationFailed { - message: err.to_string(), - } - }) - } - - /// Verify the Delaunay property via fast O(simplices) flip predicates. - /// - /// This checks the Delaunay property by testing all possible flip configurations - /// (k=2 facets, k=3 ridges, and their inverses) instead of the naive O(simplices × vertices) - /// brute-force check. This is ~40-100x faster while being equally correct. - /// - /// Ideal for property-based testing with many iterations. - /// - /// # Errors - /// - /// Returns [`DelaunayRepairError`] if any flip predicate detects a Delaunay violation. - /// - /// # Examples - /// - /// ``` - /// use delaunay::prelude::query::*; - /// - /// 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::new(&vertices).unwrap(); - /// - /// // Fast O(N) verification - /// assert!(dt.is_delaunay_via_flips().is_ok()); - /// ``` - pub fn is_delaunay_via_flips(&self) -> Result<(), DelaunayRepairError> { - verify_delaunay_for_triangulation(&self.tri) - } - - /// Performs cumulative validation for Levels 1–4. - /// - /// This validates: - /// - **Levels 1–3** via [`Triangulation::validate`](crate::triangulation::Triangulation::validate) - /// - **Level 4** via [`DelaunayTriangulation::is_valid`](Self::is_valid) - /// - /// # Errors - /// - /// Returns a [`DelaunayTriangulationValidationError`] if Levels 1–3 validation fails or if the - /// Delaunay property check (Level 4) fails. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// let vertices_4d = [ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 4> = - /// DelaunayTriangulation::new(&vertices_4d).unwrap(); - /// - /// // Levels 1–4: elements + structure + topology + Delaunay property - /// assert!(dt.validate().is_ok()); - /// ``` - pub fn validate(&self) -> Result<(), DelaunayTriangulationValidationError> { - self.tri.validate().map_err(|e| match e { - InvariantError::Tds(tds_err) => tds_err.into(), - InvariantError::Triangulation(tri_err) => tri_err.into(), - InvariantError::Delaunay(dt_err) => dt_err, - })?; - self.is_valid() - } - - /// Generate a comprehensive validation report for the full validation hierarchy. - /// - /// This is intended for debugging/telemetry (e.g. `insert_with_statistics`) where - /// you want to see *all* violated invariants, not just the first one. - /// - /// # Notes - /// - If UUID↔key mappings are inconsistent, this returns only mapping failures (other - /// checks may produce misleading secondary errors). - /// - This report is **cumulative** across Levels 1–4. - /// - /// # Errors - /// - /// Returns `Err(TriangulationValidationReport)` containing all violated invariants. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::query::*; - /// - /// let vertices = [ - /// 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::new(&vertices).unwrap(); - /// - /// // Returns Ok(()) on success; otherwise returns a report listing all violations. - /// let report = dt.validation_report(); - /// assert!(report.is_ok()); - /// ``` - pub fn validation_report(&self) -> Result<(), TriangulationValidationReport> { - // Levels 1–3: reuse the Triangulation layer report. - match self.tri.validation_report() { - Ok(()) => { - // Level 4 (Delaunay property) - if let Err(e) = self.is_valid() { - return Err(TriangulationValidationReport { - violations: vec![InvariantViolation { - kind: InvariantKind::DelaunayProperty, - error: e.into(), - }], - }); - } - Ok(()) - } - Err(mut report) => { - // If mappings are inconsistent, return the lower-layer report unchanged. - if report.violations.iter().any(|v| { - matches!( - v.kind, - InvariantKind::VertexMappings | InvariantKind::SimplexMappings - ) - }) { - return Err(report); - } - - // Level 4 (Delaunay property) - if let Err(e) = self.is_delaunay_via_flips() { - report.violations.push(InvariantViolation { - kind: InvariantKind::DelaunayProperty, - error: InvariantError::Delaunay( - DelaunayTriangulationValidationError::VerificationFailed { - message: e.to_string(), - }, - ), - }); - } - - if report.violations.is_empty() { - Ok(()) - } else { - Err(report) - } - } - } - } - // ------------------------------------------------------------------------- - // PURE STRUCT ASSEMBLY - // ------------------------------------------------------------------------- - /// Create a validated `DelaunayTriangulation` from a `Tds` with an explicit kernel. - /// - /// This is useful when you've serialized just the `Tds` and want to reconstruct - /// the `DelaunayTriangulation` with a caller-supplied kernel. The `kernel` - /// parameter provides the geometric predicates used during validation and later - /// insertions. - /// - /// # Notes - /// - /// - The internal `insertion_state.last_inserted_simplex` "locate hint" is intentionally **not** persisted - /// across serialization boundaries. Reconstructing via `try_from_tds` (including the serde - /// `Deserialize` impl below) always resets it to `None`. This can make the first few - /// insertions after loading slightly slower, but is otherwise behaviorally irrelevant. - /// - The internal spatial hash-grid index used to accelerate incremental insertion is also a - /// performance-only cache and is not serialized. Reconstructing via `try_from_tds` leaves it unset - /// so it can be rebuilt lazily on demand. - /// - The topology guarantee ([`TopologyGuarantee`]) is also not serialized (this type serializes - /// only the `Tds`). Reconstructing via `try_from_tds` resets it to `TopologyGuarantee::DEFAULT` - /// (currently `PLManifold`). Call [`set_topology_guarantee`](Self::set_topology_guarantee) - /// after loading if you want to relax to `Pseudomanifold` for performance, or use - /// [`try_from_tds_with_topology_guarantee`](Self::try_from_tds_with_topology_guarantee) to set it - /// at construction time. - /// - Runtime global topology metadata ([`GlobalTopology`]) is also not serialized. Reconstructing - /// via `try_from_tds` validates with [`GlobalTopology::Euclidean`]. Use - /// [`try_from_tds_with_topology_context`](Self::try_from_tds_with_topology_context) if you - /// need to validate toroidal or other non-default topology metadata during reconstruction. - /// - Euclidean reconstruction validates Level 4 with the crate's robust - /// empty-circumsphere validator, independent of the supplied runtime kernel. - /// The supplied kernel is stored for later queries and insertions. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::FastKernel; - /// use delaunay::prelude::tds::Tds; - /// use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0, 0.0, 0.0]), - /// vertex!([1.0, 0.0, 0.0, 0.0]), - /// vertex!([0.0, 1.0, 0.0, 0.0]), - /// vertex!([0.0, 0.0, 1.0, 0.0]), - /// vertex!([0.0, 0.0, 0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 4> = - /// DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// // Serialize just the Tds - /// let json = serde_json::to_string(dt.tds()).unwrap(); - /// - /// // Deserialize Tds and reconstruct DelaunayTriangulation - /// let tds: Tds = serde_json::from_str(&json).unwrap(); - /// let reconstructed = DelaunayTriangulation::try_from_tds(tds, FastKernel::new()).unwrap(); - /// assert_eq!(reconstructed.number_of_vertices(), 5); - /// ``` - /// - /// # Errors - /// - /// Returns [`DelaunayTriangulationValidationError`] if the TDS violates - /// structural, topological, or Delaunay invariants. - pub fn try_from_tds( - tds: Tds, - kernel: K, - ) -> Result { - Self::try_from_tds_with_topology_context( - tds, - kernel, - TopologyGuarantee::DEFAULT, - GlobalTopology::DEFAULT, - ) - } - - /// Create a validated `DelaunayTriangulation` from a `Tds` with an explicit topology guarantee. - /// - /// The candidate is assembled with the requested guarantee, then validated - /// at Levels 1–4 before being returned. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::FastKernel; - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulation, TopologyGuarantee, vertex, - /// }; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 2> = - /// DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// let reconstructed = DelaunayTriangulation::try_from_tds_with_topology_guarantee( - /// dt.tds().clone(), - /// FastKernel::new(), - /// TopologyGuarantee::PLManifoldStrict, - /// ) - /// .unwrap(); - /// - /// assert_eq!( - /// reconstructed.topology_guarantee(), - /// TopologyGuarantee::PLManifoldStrict - /// ); - /// ``` - /// - /// # Errors - /// - /// Returns [`DelaunayTriangulationValidationError`] if the TDS violates - /// structural, topological, or Delaunay invariants. - pub fn try_from_tds_with_topology_guarantee( - tds: Tds, - kernel: K, - topology_guarantee: TopologyGuarantee, - ) -> Result { - Self::try_from_tds_with_topology_context( - tds, - kernel, - topology_guarantee, - GlobalTopology::DEFAULT, - ) - } - - /// Create a validated `DelaunayTriangulation` from a `Tds` with explicit topology context. - /// - /// This is the checked reconstruction path for serialized TDS data whose - /// runtime [`TopologyGuarantee`] or [`GlobalTopology`] metadata must be - /// restored before validation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::geometry::FastKernel; - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulation, GlobalTopology, TopologyGuarantee, vertex, - /// }; - /// - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 2> = - /// DelaunayTriangulation::new(&vertices).unwrap(); - /// - /// let reconstructed = DelaunayTriangulation::try_from_tds_with_topology_context( - /// dt.tds().clone(), - /// FastKernel::new(), - /// TopologyGuarantee::PLManifoldStrict, - /// GlobalTopology::Euclidean, - /// ) - /// .unwrap(); - /// - /// assert_eq!( - /// reconstructed.topology_guarantee(), - /// TopologyGuarantee::PLManifoldStrict - /// ); - /// assert_eq!(reconstructed.global_topology(), GlobalTopology::Euclidean); - /// ``` - /// - /// # Errors - /// - /// Returns [`DelaunayTriangulationValidationError`] if the TDS violates - /// structural, topological, or Delaunay invariants under the supplied - /// topology context. - pub fn try_from_tds_with_topology_context( - tds: Tds, - kernel: K, - topology_guarantee: TopologyGuarantee, - global_topology: GlobalTopology, - ) -> Result { - let mut candidate = Self::from_tds_with_topology_guarantee(tds, kernel, topology_guarantee); - candidate.set_global_topology(global_topology); - candidate.tri.validate().map_err(|e| match e { - InvariantError::Tds(tds_err) => tds_err.into(), - InvariantError::Triangulation(tri_err) => tri_err.into(), - InvariantError::Delaunay(dt_err) => dt_err, - })?; - - if candidate.global_topology().is_euclidean() { - is_delaunay_property_only(&candidate.tri.tds).map_err(|e| { - DelaunayTriangulationValidationError::VerificationFailed { - message: format!("kernel-independent reconstruction validation failed: {e}"), - } - })?; - } else { - candidate.is_valid()?; - } - Ok(candidate) - } - - /// Assemble a `DelaunayTriangulation` from a `Tds` with an explicit topology guarantee. - /// - /// This crate-internal constructor performs no validation; public callers - /// must use [`try_from_tds_with_topology_guarantee`](Self::try_from_tds_with_topology_guarantee). - /// The initial - /// [`ValidationPolicy`] is derived from the guarantee: - /// [`PLManifoldStrict`](TopologyGuarantee::PLManifoldStrict) uses - /// [`Always`](ValidationPolicy::Always); all others default to - /// [`OnSuspicion`](ValidationPolicy::OnSuspicion). - #[must_use] - pub(crate) const fn from_tds_with_topology_guarantee( - tds: Tds, - kernel: K, - topology_guarantee: TopologyGuarantee, - ) -> Self { - let validation_policy = topology_guarantee.default_validation_policy(); - Self { - tri: Triangulation { - kernel, - tds, - global_topology: GlobalTopology::DEFAULT, - validation_policy, - topology_guarantee, - }, - insertion_state: DelaunayInsertionState::new(), - spatial_index: None, - } - } -} - -// Custom Serialize implementation that only serializes the Tds -impl Serialize for DelaunayTriangulation -where - K: Kernel, - U: DataType, - V: DataType, -{ - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // Only serialize the Tds; kernel can be reconstructed on deserialization - self.tri.tds.serialize(serializer) - } -} - -/// Custom `Deserialize` implementation for `RobustKernel` with no custom data. -/// -/// Kernels are stateless and can be reconstructed on deserialization. We only -/// serialize the `Tds` (which contains all the geometric and topological data), -/// then reconstruct the kernel wrapper on deserialization. -/// -/// # Note on Locate Hint Persistence -/// -/// The internal `insertion_state.last_inserted_simplex` "locate hint" is intentionally -/// **not** serialized. Deserialization reconstructs a fresh triangulation via -/// [`try_from_tds()`](Self::try_from_tds), which resets the hint to `None`. This only -/// affects performance for the first few insertions after loading. -/// -/// # Usage with Other Kernels -/// -/// For other kernels (e.g., `AdaptiveKernel`, `FastKernel`) or custom data types, -/// deserialize the `Tds` directly and reconstruct with [`try_from_tds()`](Self::try_from_tds): -/// -/// ```rust -/// # use delaunay::prelude::geometry::*; -/// # use delaunay::prelude::tds::Tds; -/// # use delaunay::prelude::triangulation::construction::{DelaunayTriangulation, vertex}; -/// # fn example() { -/// // Create and serialize a triangulation -/// 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>::new(&vertices) -/// .expect("nondegenerate tetrahedron should construct"); -/// let json = serde_json::to_string(&dt).expect("triangulation should serialize"); -/// -/// // Deserialize with a specific kernel via try_from_tds -/// let tds: Tds = -/// serde_json::from_str(&json).expect("serialized triangulation should deserialize"); -/// let dt_adaptive = DelaunayTriangulation::try_from_tds(tds, AdaptiveKernel::new()) -/// .expect("deserialized TDS should validate"); -/// # } -/// ``` -impl<'de, const D: usize> Deserialize<'de> for DelaunayTriangulation, (), (), D> -where - Tds: Deserialize<'de>, -{ - fn deserialize(deserializer: De) -> Result - where - De: Deserializer<'de>, - { - let tds = Tds::deserialize(deserializer)?; - Self::try_from_tds(tds, RobustKernel::new()).map_err(serde::de::Error::custom) - } -} - -/// Policy controlling automatic flip-based Delaunay repair. -/// -/// This policy schedules **local flip-based repairs** after successful insertions -/// (and removals that modify topology). -/// It is separate from any *validation-only* policy to allow checking the Delaunay -/// property without mutating topology when needed. -/// -/// During batch construction, [`DelaunayRepairPolicy::EveryN`] is a scheduled -/// cadence rather than a hard lower bound on repair frequency: construction may -/// run an additional local repair earlier when the accumulated seed frontier -/// grows large. [`DelaunayRepairPolicy::Never`] disables those automatic batch -/// repairs. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::repair::DelaunayRepairPolicy; -/// use std::num::NonZeroUsize; -/// -/// let policy = DelaunayRepairPolicy::EveryN(NonZeroUsize::new(4).unwrap()); -/// assert!(!policy.should_repair(0)); -/// assert!(!policy.should_repair(3)); -/// assert!(policy.should_repair(4)); -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DelaunayRepairPolicy { - /// Disable automatic Delaunay repairs. - Never, - /// Run local flip-based repair after every successful insertion. - EveryInsertion, - /// Run local flip-based repair after every N successful insertions. - EveryN(NonZeroUsize), -} - -impl Default for DelaunayRepairPolicy { - #[inline] - fn default() -> Self { - Self::EveryInsertion - } -} - -impl DelaunayRepairPolicy { - /// Returns true if a repair pass should run after the given insertion count. - #[inline] - #[must_use] - pub const fn should_repair(self, insertion_count: usize) -> bool { - match self { - Self::Never => false, - Self::EveryInsertion => insertion_count != 0, - Self::EveryN(n) => insertion_count != 0 && insertion_count.is_multiple_of(n.get()), - } - } -} -/// Configuration for the optional heuristic rebuild fallback in Delaunay repair. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::repair::DelaunayRepairHeuristicConfig; -/// -/// let mut config = DelaunayRepairHeuristicConfig::default(); -/// config.shuffle_seed = Some(7); -/// config.perturbation_seed = Some(11); -/// assert_eq!(config.shuffle_seed, Some(7)); -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -#[non_exhaustive] -pub struct DelaunayRepairHeuristicConfig { - /// Optional RNG seed used to shuffle vertex insertion order. - pub shuffle_seed: Option, - /// Optional seed used to vary the deterministic perturbation pattern. - pub perturbation_seed: Option, - /// Optional per-attempt flip budget cap. - /// - /// When set, each repair attempt is limited to at most this many flips. - /// `None` (the default) uses the dimension-dependent internal budget - /// computed from the triangulation size. - /// - /// This is primarily useful for debug harnesses that want to study - /// repair convergence behavior at different budgets without disabling - /// repair entirely. - pub max_flips: Option, -} - -impl DelaunayRepairHeuristicConfig { - /// Fills omitted seeds from a stable base so heuristic rebuilds are - /// repeatable even when callers only configure one axis of randomness. - fn resolve_seeds(self, base_seed: u64) -> DelaunayRepairHeuristicSeeds { - // Derive deterministic defaults when the caller does not provide explicit seeds. - const SHUFFLE_SALT: u64 = 0x9E37_79B9_7F4A_7C15; - const PERTURB_SALT: u64 = 0xD1B5_4A32_D192_ED03; - - let mut shuffle_seed = self - .shuffle_seed - .unwrap_or_else(|| base_seed.wrapping_add(SHUFFLE_SALT)); - if self.shuffle_seed.is_none() && shuffle_seed == 0 { - shuffle_seed = 1; - } - - let mut perturbation_seed = self - .perturbation_seed - .unwrap_or_else(|| base_seed.rotate_left(17) ^ PERTURB_SALT); - if self.perturbation_seed.is_none() && perturbation_seed == 0 { - perturbation_seed = 1; - } - - DelaunayRepairHeuristicSeeds { - shuffle_seed, - perturbation_seed, - } - } -} - -/// Seeds used for a heuristic rebuild. -/// -/// If the caller does not provide explicit seeds, deterministic defaults are derived from a stable -/// hash of the current vertex set. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::repair::DelaunayRepairHeuristicSeeds; -/// -/// let seeds = DelaunayRepairHeuristicSeeds { -/// shuffle_seed: 1, -/// perturbation_seed: 2, -/// }; -/// assert_eq!(seeds.shuffle_seed, 1); -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DelaunayRepairHeuristicSeeds { - /// RNG seed used to shuffle vertex insertion order. - pub shuffle_seed: u64, - /// Seed used to vary the perturbation pattern during retries. - pub perturbation_seed: u64, -} - -/// Result of a flip-based repair attempt, including heuristic fallback metadata. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::repair::{ -/// DelaunayRepairOutcome, DelaunayRepairStats, -/// }; -/// -/// let outcome = DelaunayRepairOutcome { -/// stats: DelaunayRepairStats::default(), -/// heuristic: None, -/// }; -/// assert!(!outcome.used_heuristic()); -/// ``` -#[derive(Debug, Clone)] -pub struct DelaunayRepairOutcome { - /// Statistics from the final flip-based repair pass. - pub stats: DelaunayRepairStats, - /// Heuristic rebuild seeds, if a fallback was used. - pub heuristic: Option, -} - -impl DelaunayRepairOutcome { - /// Returns `true` if a heuristic rebuild fallback was used. - #[must_use] - pub const fn used_heuristic(&self) -> bool { - self.heuristic.is_some() - } -} - -/// Policy controlling when **global** Delaunay validation runs. -/// -/// This policy is **validation-only** (non-mutating) and is distinct from -/// [`DelaunayRepairPolicy`], which performs flip-based repairs. -/// -/// # ⚠️ Performance Warning -/// -/// Global Delaunay validation is **extremely expensive**: O(simplices × vertices). Use this policy -/// primarily when you need correctness guarantees and are willing to pay the cost. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::repair::DelaunayCheckPolicy; -/// use std::num::NonZeroUsize; -/// -/// let policy = DelaunayCheckPolicy::EveryN(NonZeroUsize::new(3).unwrap()); -/// assert!(!policy.should_check(2)); -/// assert!(policy.should_check(3)); -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum DelaunayCheckPolicy { - /// Run global Delaunay validation once after batch construction (e.g. `new()` / `with_kernel()`). - /// - /// Incremental insertion does not automatically run a final global check because there is no - /// intrinsic “end” signal; call [`DelaunayTriangulation::is_valid`] or - /// [`DelaunayTriangulation::validate`] when you are done inserting. - #[default] - EndOnly, - /// Run global Delaunay validation after every N successful insertions. - EveryN(NonZeroUsize), -} - -impl DelaunayCheckPolicy { - /// Returns true if a global Delaunay validation pass should run after the given insertion count. - #[inline] - #[must_use] - pub const fn should_check(self, insertion_count: usize) -> bool { - match self { - Self::EndOnly => false, - Self::EveryN(n) => insertion_count.is_multiple_of(n.get()), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::core::algorithms::flips::{ - DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairVerificationContext, - FlipContextError, FlipError, FlipPredicateError, FlipPredicateOperation, RepairQueueOrder, - verify_delaunay_via_flip_predicates, - }; - use crate::core::algorithms::incremental_insertion::{ - CavityFillingError, HullExtensionReason, NeighborWiringError, - }; - use crate::core::algorithms::locate::{ConflictError, LocateError}; - use crate::core::operations::InsertionResult; - use crate::core::tds::{EntityKind, GeometricError, TriangulationConstructionState}; - use crate::core::vertex::VertexBuilder; - use crate::geometry::kernel::{AdaptiveKernel, FastKernel, RobustKernel}; - use crate::geometry::point::Point; - use crate::geometry::traits::coordinate::{Coordinate, CoordinateConversionError}; - use crate::topology::characteristics::euler::TopologyClassification; - use crate::topology::traits::topological_space::ToroidalConstructionMode; - use crate::triangulation::flips::BistellarFlips; - use crate::vertex; - use slotmap::KeyData; - use std::{collections::HashSet, error::Error as StdError, sync::Once}; - - type TestDelaunay = DelaunayTriangulation, (), (), D>; - - fn init_tracing() { - static INIT: Once = Once::new(); - INIT.call_once(|| { - let filter = tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")); - let _ = tracing_subscriber::fmt() - .with_env_filter(filter) - .with_test_writer() - .try_init(); - }); - } - - fn wedge_two_spheres_share_vertex_tds_2d() -> (Tds, SimplexKey, SimplexKey) { - // Two closed 2D spheres (boundaries of tetrahedra) sharing one vertex are - // pseudomanifold but not PL-manifold: the shared vertex has a disconnected link. - let mut tds: Tds = Tds::empty(); - - let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); - let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); - let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); - let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap(); - - let incident = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v3], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v2, v3], None).unwrap()) - .unwrap(); - let nonincident = tds - .insert_simplex_with_mapping(Simplex::new(vec![v1, v2, v3], None).unwrap()) - .unwrap(); - - let v4 = tds - .insert_vertex_with_mapping(vertex!([10.0, 10.0])) - .unwrap(); - let v5 = tds - .insert_vertex_with_mapping(vertex!([11.0, 10.0])) - .unwrap(); - let v6 = tds - .insert_vertex_with_mapping(vertex!([10.0, 11.0])) - .unwrap(); - - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v4, v5], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v4, v6], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v0, v5, v6], None).unwrap()) - .unwrap(); - let _ = tds - .insert_simplex_with_mapping(Simplex::new(vec![v4, v5, v6], None).unwrap()) - .unwrap(); - - (tds, incident, nonincident) - } - - #[test] - fn test_ridge_link_repair_validation_error_routes_tds_errors_to_tds_layer() { - let tds_err = TdsError::InvalidNeighbors { - reason: NeighborValidationError::Other { - message: "unit test".to_string(), - }, - }; - - match ridge_link_repair_validation_error(ManifoldError::Tds(tds_err.clone())) { - InsertionError::TopologyValidation(source) => assert_eq!(source, tds_err), - other => panic!("expected TopologyValidation, got {other:?}"), - } - } - - #[test] - fn test_ridge_link_repair_validation_error_routes_manifold_errors_to_triangulation_layer() { - let ridge_key = 0x1234_u64; - let error = ridge_link_repair_validation_error(ManifoldError::BoundaryRidgeMultiplicity { - ridge_key, - boundary_facet_count: 3, - }); - - match error { - InsertionError::TopologyValidationFailed { message, source } => { - assert_eq!(message, RIDGE_LINK_REPAIR_VALIDATION_MESSAGE); - assert!(matches!( - source, - TriangulationValidationError::BoundaryRidgeMultiplicity { - ridge_key: observed_ridge_key, - boundary_facet_count: 3 - } if observed_ridge_key == ridge_key - )); - } - other => panic!("expected TopologyValidationFailed, got {other:?}"), - } - } - - macro_rules! gen_local_repair_flip_budget_tests { - ($dim:literal, $floor:ident, $factor:ident) => { - pastey::paste! { - #[test] - fn []() { - assert_eq!(local_repair_flip_budget::<$dim>(0), $floor); - - let seed_count = 10; - let raw = seed_count * ($dim + 1) * $factor; - assert_eq!(local_repair_flip_budget::<$dim>(seed_count), raw.max($floor)); - } - } - }; - } - - gen_local_repair_flip_budget_tests!( - 2, - LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_LT_4, - LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_LT_4 - ); - gen_local_repair_flip_budget_tests!( - 3, - LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_LT_4, - LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_LT_4 - ); - gen_local_repair_flip_budget_tests!( - 4, - LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_GE_4, - LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_GE_4 - ); - gen_local_repair_flip_budget_tests!( - 5, - LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_GE_4, - LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_GE_4 - ); - - #[test] - fn test_local_repair_seed_backlog_threshold_uses_dimension_regimes() { - assert_eq!( - local_repair_seed_backlog_threshold::<3>(), - 4 * LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_LT_4 - ); - assert_eq!( - local_repair_seed_backlog_threshold::<4>(), - 5 * LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_GE_4 - ); - } - - #[test] - fn test_batch_local_repair_trigger_prefers_cadence_over_backlog() { - let policy = DelaunayRepairPolicy::EveryN(NonZeroUsize::new(4).unwrap()); - let threshold = local_repair_seed_backlog_threshold::<3>(); - - assert_eq!( - batch_local_repair_trigger::<3>(policy, 4, TopologyGuarantee::PLManifold, threshold), - Some(BatchLocalRepairTrigger::Cadence) - ); - } - - #[test] - fn test_batch_local_repair_trigger_runs_every_insertion_below_backlog() { - assert_eq!( - batch_local_repair_trigger::<3>( - DelaunayRepairPolicy::EveryInsertion, - 1, - TopologyGuarantee::PLManifold, - 1, - ), - Some(BatchLocalRepairTrigger::Cadence) - ); - assert_eq!( - batch_local_repair_trigger::<3>( - DelaunayRepairPolicy::EveryInsertion, - 0, - TopologyGuarantee::PLManifold, - 1, - ), - None - ); - } - - #[test] - fn test_batch_local_repair_trigger_repairs_early_on_seed_backlog() { - let policy = DelaunayRepairPolicy::EveryN(NonZeroUsize::new(128).unwrap()); - let threshold = local_repair_seed_backlog_threshold::<3>(); - - assert_eq!( - batch_local_repair_trigger::<3>(policy, 7, TopologyGuarantee::PLManifold, threshold), - Some(BatchLocalRepairTrigger::SeedBacklog) - ); - assert_eq!( - batch_local_repair_trigger::<3>( - policy, - 7, - TopologyGuarantee::PLManifold, - threshold - 1 - ), - None - ); - } - - #[test] - fn test_batch_local_repair_trigger_respects_policy_and_topology() { - let threshold = local_repair_seed_backlog_threshold::<3>(); - - assert_eq!( - batch_local_repair_trigger::<3>( - DelaunayRepairPolicy::Never, - 7, - TopologyGuarantee::PLManifold, - threshold - ), - None - ); - assert_eq!( - batch_local_repair_trigger::<3>( - DelaunayRepairPolicy::EveryN(NonZeroUsize::new(128).unwrap()), - 7, - TopologyGuarantee::PLManifold, - 0 - ), - None - ); - assert_eq!( - batch_local_repair_trigger::<3>( - DelaunayRepairPolicy::EveryN(NonZeroUsize::new(128).unwrap()), - 7, - TopologyGuarantee::Pseudomanifold, - threshold - ), - Some(BatchLocalRepairTrigger::SeedBacklog) - ); - } - - #[test] - fn test_log_bulk_progress_if_due_updates_progress_state_only_when_due() { - let sample = BatchProgressSample { - bulk_processed: 5, - bulk_inserted: 4, - bulk_skipped: 1, - simplex_count: 7, - perturbation_seed: 0xCAFE, - }; - - let mut disabled = None; - log_bulk_progress_if_due(sample, &mut disabled); - assert!(disabled.is_none()); - - let mut state = Some(BatchProgressState { - input_vertices: 13, - initial_simplex_vertices: 3, - bulk_vertices: 10, - progress_every: 5, - started: Instant::now(), - last_progress: Instant::now(), - last_processed: 0, - }); - - log_bulk_progress_if_due( - BatchProgressSample { - bulk_processed: 0, - ..sample - }, - &mut state, - ); - assert_eq!(state.as_ref().unwrap().last_processed, 0); - - log_bulk_progress_if_due( - BatchProgressSample { - bulk_processed: 3, - ..sample - }, - &mut state, - ); - assert_eq!(state.as_ref().unwrap().last_processed, 0); - - log_bulk_progress_if_due(sample, &mut state); - assert_eq!(state.as_ref().unwrap().last_processed, 5); - - log_bulk_progress_if_due( - BatchProgressSample { - bulk_processed: 10, - bulk_inserted: 8, - bulk_skipped: 2, - simplex_count: 11, - perturbation_seed: 0xBEEF, - }, - &mut state, - ); - assert_eq!(state.as_ref().unwrap().last_processed, 10); - } - - #[test] - fn test_collect_local_repair_seed_simplices_merges_adjacent_extra_and_ignores_stale() { - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([1.0, 1.0]), - vertex!([0.5, 0.5]), - ]; - let dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - let all_simplices: Vec = - dt.simplices().map(|(simplex_key, _)| simplex_key).collect(); - - let (vertex_key, adjacent, extra_simplex) = dt - .vertices() - .find_map(|(vertex_key, _)| { - let adjacent: Vec = dt.tri.adjacent_simplices(vertex_key).collect(); - all_simplices - .iter() - .copied() - .find(|simplex_key| !adjacent.contains(simplex_key)) - .map(|extra_simplex| (vertex_key, adjacent, extra_simplex)) - }) - .expect("fixture should contain a simplex outside at least one vertex star"); - - let stale_simplex = SimplexKey::from(KeyData::from_ffi(999_999)); - let seeds = dt.collect_local_repair_seed_simplices( - vertex_key, - &[adjacent[0], extra_simplex, extra_simplex, stale_simplex], - ); - - assert_eq!(seeds.len(), adjacent.len() + 1); - assert_eq!(&seeds[..adjacent.len()], adjacent.as_slice()); - assert_eq!(seeds[adjacent.len()], extra_simplex); - assert!(!seeds.contains(&stale_simplex)); - } - - #[test] - fn test_validate_ridge_links_after_full_reseed_repair_uses_mutation_frontier() { - init_tracing(); - let (tds, incident_to_invalid_ridge, nonincident) = wedge_two_spheres_share_vertex_tds_2d(); - let dt = DelaunayTriangulation::from_tds_with_topology_guarantee( - tds, - AdaptiveKernel::new(), - TopologyGuarantee::PLManifold, - ); - let stats = DelaunayRepairStats { - flips_performed: 1, - ..DelaunayRepairStats::default() - }; - - let local_run = DelaunayRepairRun { - stats: stats.clone(), - touched_simplices: std::iter::once(nonincident).collect(), - used_full_reseed: true, - }; - assert!( - dt.validate_ridge_links_after_repair(TopologyGuarantee::PLManifold, &local_run) - .is_ok() - ); - - let invalid_scope_run = DelaunayRepairRun { - stats, - touched_simplices: std::iter::once(incident_to_invalid_ridge).collect(), - used_full_reseed: true, - }; - assert!( - dt.validate_ridge_links_after_repair( - TopologyGuarantee::PLManifold, - &invalid_scope_run, - ) - .is_err() - ); - } - - struct ForceHeuristicRebuildGuard { - prior: bool, - } - - impl ForceHeuristicRebuildGuard { - fn enable() -> Self { - let prior = test_hooks::set_force_heuristic_rebuild(true); - Self { prior } - } - } - - impl Drop for ForceHeuristicRebuildGuard { - fn drop(&mut self) { - test_hooks::restore_force_heuristic_rebuild(self.prior); - } - } - - struct ForceRepairNonconvergentGuard { - prior: bool, - } - - impl ForceRepairNonconvergentGuard { - fn enable() -> Self { - let prior = test_hooks::set_force_repair_nonconvergent(true); - Self { prior } - } - } - - impl Drop for ForceRepairNonconvergentGuard { - fn drop(&mut self) { - test_hooks::restore_force_repair_nonconvergent(self.prior); - } - } - - #[test] - fn test_construction_options_default_uses_batch_repair_cadence() { - init_tracing(); - assert_eq!( - ConstructionOptions::default().initial_simplex_strategy(), - InitialSimplexStrategy::MaxVolume - ); - assert_eq!( - ConstructionOptions::default().batch_repair_policy(), - DelaunayRepairPolicy::EveryInsertion - ); - assert_eq!( - DelaunayRepairPolicy::default(), - DelaunayRepairPolicy::EveryInsertion - ); - } - - #[test] - fn test_construction_options_builder_roundtrip() { - init_tracing(); - let opts = ConstructionOptions::default() - .with_insertion_order(InsertionOrderStrategy::Input) - .with_dedup_policy(DedupPolicy::Exact) - .with_batch_repair_policy(DelaunayRepairPolicy::EveryN(NonZeroUsize::new(4).unwrap())) - .with_retry_policy(RetryPolicy::Disabled); - - assert_eq!(opts.insertion_order(), InsertionOrderStrategy::Input); - assert_eq!(opts.dedup_policy(), DedupPolicy::Exact); - assert_eq!( - opts.batch_repair_policy(), - DelaunayRepairPolicy::EveryN(NonZeroUsize::new(4).unwrap()) - ); - assert_eq!(opts.retry_policy(), RetryPolicy::Disabled); - } - - #[test] - fn test_construction_options_global_repair_fallback_toggle() { - init_tracing(); - let default_opts = ConstructionOptions::default(); - assert!( - default_opts.use_global_repair_fallback, - "default should enable global repair fallback" - ); - - let disabled_opts = default_opts.without_global_repair_fallback(); - assert!( - !disabled_opts.use_global_repair_fallback, - "without_global_repair_fallback should disable the flag" - ); - - // Chaining with other builders should preserve the flag. - let chained_opts = ConstructionOptions::default() - .with_insertion_order(InsertionOrderStrategy::Input) - .without_global_repair_fallback() - .with_retry_policy(RetryPolicy::Disabled); - assert!(!chained_opts.use_global_repair_fallback); - assert_eq!( - chained_opts.insertion_order(), - InsertionOrderStrategy::Input - ); - assert_eq!(chained_opts.retry_policy(), RetryPolicy::Disabled); - } - - #[test] - fn test_new_with_options_smoke_3d() { - init_tracing(); - let vertices: Vec> = 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 opts = ConstructionOptions::default().with_retry_policy(RetryPolicy::Disabled); - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new_with_options(&vertices, opts).unwrap(); - - assert_eq!(dt.number_of_vertices(), 4); - assert_eq!(dt.number_of_simplices(), 1); - assert!(dt.validate().is_ok()); - } - - #[test] - fn test_new_with_construction_statistics_counts_initial_simplex_3d() { - init_tracing(); - let vertices: Vec> = 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, stats) = - DelaunayTriangulation::new_with_construction_statistics(&vertices).unwrap(); - - assert_eq!(dt.number_of_vertices(), 4); - assert_eq!(stats.inserted, 4); - assert_eq!(stats.total_skipped(), 0); - assert_eq!(stats.total_attempts, 4); - assert_eq!(stats.max_attempts, 1); - assert_eq!(stats.attempts_histogram.get(1).copied().unwrap_or(0), 4); - } - - #[test] - fn test_new_with_options_and_construction_statistics_skips_duplicate_3d() { - init_tracing(); - let vertices: Vec> = vec![ - // Initial simplex - 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]), - // Duplicate coords (different UUID) - vertex!([0.0, 0.0, 0.0]), - ]; - let duplicate_uuid = vertices[4].uuid(); - - let opts = ConstructionOptions::default() - .with_insertion_order(InsertionOrderStrategy::Input) - .with_retry_policy(RetryPolicy::Disabled); - - let (dt, stats) = - DelaunayTriangulation::new_with_options_and_construction_statistics(&vertices, opts) - .unwrap(); - - assert_eq!(dt.number_of_vertices(), 4); - assert_eq!(stats.inserted, 4); - assert_eq!(stats.skipped_duplicate, 1); - assert_eq!(stats.skipped_degeneracy, 0); - assert_eq!(stats.total_skipped(), 1); - assert_eq!(stats.total_attempts, 5); - assert_eq!(stats.attempts_histogram.get(1).copied().unwrap_or(0), 5); - - assert_eq!(stats.skip_samples.len(), 1); - let sample = &stats.skip_samples[0]; - assert_eq!(sample.index, 4); - assert_eq!(sample.uuid, duplicate_uuid); - assert_eq!(sample.coords, vec![0.0, 0.0, 0.0]); - assert!(sample.coords_available); - assert_eq!(sample.attempts, 1); - assert!(sample.error.contains("Duplicate coordinates")); - } - - #[test] - fn test_vertex_coords_f64_converts_f64_vertex_coords() { - init_tracing(); - let vertex: Vertex = vertex!([1.25, -2.5, 3.75]); - - assert_eq!(vertex_coords_f64(&vertex), Some(vec![1.25, -2.5, 3.75])); - } - - #[test] - fn test_vertex_coords_f64_converts_f32_vertex_coords() { - init_tracing(); - let vertex: Vertex = vertex!([1.25f32, -2.5f32, 3.75f32]); - - assert_eq!(vertex_coords_f64(&vertex), Some(vec![1.25, -2.5, 3.75])); - } - - #[test] - fn test_vertex_coords_f64_rejects_non_finite_coords() { - init_tracing(); - let nan_vertex: Vertex = VertexBuilder::default() - .point(Point::new([1.0, f64::NAN, 3.0])) - .build() - .unwrap(); - let infinite_vertex: Vertex = VertexBuilder::default() - .point(Point::new([1.0, f64::INFINITY, 3.0])) - .build() - .unwrap(); - - assert_eq!(vertex_coords_f64(&nan_vertex), None); - assert_eq!(vertex_coords_f64(&infinite_vertex), None); - } - - fn coord_sequence_2d(vertices: &[Vertex]) -> Vec<[f64; 2]> { - vertices.iter().map(|v| *v.point().coords()).collect() - } - - #[test] - fn order_vertices_input_preserves_order() { - init_tracing(); - let vertices = vec![ - vertex!([2.0, 0.0]), - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - ]; - let expected = coord_sequence_2d(&vertices); - - let ordered = order_vertices_by_strategy(vertices, InsertionOrderStrategy::Input); - - assert_eq!(coord_sequence_2d(&ordered), expected); - } - - #[test] - fn dedup_exact_sorted_without_grid() { - init_tracing(); - let vertices = vec![ - vertex!([1.0, 0.0]), - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - - let unique = dedup_vertices_exact_sorted(vertices); - - assert_eq!( - coord_sequence_2d(&unique), - vec![[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]] - ); - } - - #[test] - fn dedup_exact_grid_fallback() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 0.0]), - ]; - let mut grid = HashGridIndex::::new(1.0e-10); - - let unique = dedup_vertices_exact_hash_grid(vertices, &mut grid); - - assert_eq!(coord_sequence_2d(&unique), vec![[0.0, 0.0], [1.0, 0.0]]); - - let vertices_6d = vec![ - vertex!([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - ]; - let mut unusable_grid = HashGridIndex::::new(1.0e-10); - - let fallback_unique = dedup_vertices_exact_hash_grid(vertices_6d, &mut unusable_grid); - - assert_eq!(fallback_unique.len(), 1); - } - - #[test] - fn epsilon_dedup_quantized_paths() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([0.09, 0.0]), - vertex!([0.25, 0.0]), - ]; - - let unique = dedup_vertices_epsilon_quantized(vertices, 0.1); - - assert_eq!(coord_sequence_2d(&unique), vec![[0.0, 0.0], [0.25, 0.0]]); - - let zero_epsilon_vertices = vec![vertex!([0.0, 0.0]), vertex!([0.0, 0.0])]; - let zero_epsilon_unique = dedup_vertices_epsilon_quantized(zero_epsilon_vertices, 0.0); - assert_eq!(zero_epsilon_unique.len(), 2); - - let nonfinite_vertices = vec![ - vertex!([0.0, 0.0]), - Vertex::new_with_uuid(Point::new([f64::NAN, 0.0]), Uuid::new_v4(), None), - ]; - let nonfinite_unique = dedup_vertices_epsilon_quantized(nonfinite_vertices, 0.1); - assert_eq!(nonfinite_unique.len(), 2); - - let vertices_6d = vec![ - vertex!([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - vertex!([0.01, 0.0, 0.0, 0.0, 0.0, 0.0]), - ]; - let fallback_unique = dedup_vertices_epsilon_quantized(vertices_6d, 0.1); - assert_eq!(fallback_unique.len(), 1); - } - - #[test] - fn dedup_epsilon_grid_fallback() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([0.05, 0.0]), - vertex!([0.25, 0.0]), - ]; - let mut grid = HashGridIndex::::new(0.1); - - let unique = dedup_vertices_epsilon_hash_grid(vertices, 0.1, &mut grid); - - assert_eq!(coord_sequence_2d(&unique), vec![[0.0, 0.0], [0.25, 0.0]]); - - let fallback_vertices = vec![vertex!([0.0, 0.0]), vertex!([0.05, 0.0])]; - let mut unusable_grid = HashGridIndex::::new(0.0); - - let fallback_unique = - dedup_vertices_epsilon_hash_grid(fallback_vertices, 0.1, &mut unusable_grid); - - assert_eq!(fallback_unique.len(), 1); - } - - #[test] - fn preprocess_falls_back_when_grid_unusable() { - init_tracing(); - let exact_vertices = vec![ - vertex!([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - vertex!([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - ]; - - let exact = TestDelaunay::<6>::preprocess_vertices_for_construction( - &exact_vertices, - DedupPolicy::Exact, - InsertionOrderStrategy::Input, - InitialSimplexStrategy::First, - ) - .unwrap(); - - assert_eq!(exact.primary_slice(&exact_vertices).len(), 2); - assert!(exact.grid_cell_size().is_none()); - - let epsilon_vertices = vec![ - vertex!([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), - vertex!([0.01, 0.0, 0.0, 0.0, 0.0, 0.0]), - vertex!([0.5, 0.0, 0.0, 0.0, 0.0, 0.0]), - ]; - - let epsilon = TestDelaunay::<6>::preprocess_vertices_for_construction( - &epsilon_vertices, - DedupPolicy::Epsilon { tolerance: 0.1 }, - InsertionOrderStrategy::Input, - InitialSimplexStrategy::First, - ) - .unwrap(); - - assert_eq!(epsilon.primary_slice(&epsilon_vertices).len(), 2); - assert!(epsilon.grid_cell_size().is_none()); - } - - #[test] - fn preprocess_zero_epsilon_keeps_base() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0, 0.0]), - vertex!([0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0]), - ]; - - let preprocess = TestDelaunay::<3>::preprocess_vertices_for_construction( - &vertices, - DedupPolicy::Epsilon { tolerance: 0.0 }, - InsertionOrderStrategy::Input, - InitialSimplexStrategy::Balanced, - ) - .unwrap(); - - assert!(preprocess.grid_cell_size().is_some()); - assert_eq!(preprocess.primary_slice(&vertices).len(), vertices.len()); - assert!(preprocess.fallback_slice().is_none()); - } - - #[test] - fn quantize_and_neighbor_edges() { - init_tracing(); - assert_eq!(quantize_coords(&[0.25, -0.25], 10.0), Some([2, -3])); - assert_eq!(quantize_coords(&[f64::NAN, 0.0], 10.0), None); - assert_eq!(quantize_coords(&[1.0e308, 0.0], 1.0e308), None); - - let mut visited = Vec::new(); - let mut current = [0_i64, 0_i64]; - let completed = visit_quantized_neighbors(0, &[4, 7], &mut current, &mut |neighbor| { - visited.push(neighbor); - visited.len() < 4 - }); - - assert!(!completed); - assert_eq!(visited.len(), 4); - } - - #[test] - fn hilbert_fallback_for_nonfinite_coords() { - init_tracing(); - let vertices = vec![ - vertex!([1.0, 0.0]), - Vertex::new_with_uuid(Point::new([f64::NAN, 0.0]), Uuid::new_v4(), None), - vertex!([0.0, 0.0]), - ]; - - let ordered = order_vertices_hilbert(vertices, true); - - assert_eq!(ordered.len(), 3); - assert!( - ordered.iter().any(|v| v.point().coords()[0].is_nan()), - "fallback ordering should preserve the non-finite vertex" - ); - assert!( - ordered - .iter() - .any(|v| coords_equal_exact(v.point().coords(), &[0.0, 0.0])) - ); - assert!( - ordered - .iter() - .any(|v| coords_equal_exact(v.point().coords(), &[1.0, 0.0])) - ); - } - - #[test] - fn hilbert_fallback_for_unsupported_dim() { - init_tracing(); - let vertices = vec![vertex!([1.0; 17]), vertex!([0.0; 17])]; - - let ordered = order_vertices_hilbert(vertices, true); - - assert!(coords_equal_exact(ordered[0].point().coords(), &[0.0; 17])); - assert!(coords_equal_exact(ordered[1].point().coords(), &[1.0; 17])); - } - - #[test] - fn test_construction_statistics_record_insertion_tracks_inserted_common_fields() { - init_tracing(); - - let mut summary = ConstructionStatistics::default(); - let stats = InsertionStatistics { - attempts: 3, - simplices_removed_during_repair: 4, - result: InsertionResult::Inserted, - }; - - summary.record_insertion(&stats); - - assert_eq!(summary.inserted, 1); - assert_eq!(summary.skipped_duplicate, 0); - assert_eq!(summary.skipped_degeneracy, 0); - assert_eq!(summary.total_attempts, 3); - assert_eq!(summary.max_attempts, 3); - assert_eq!(summary.attempts_histogram.get(3).copied().unwrap_or(0), 1); - assert_eq!(summary.used_perturbation, 1); - assert_eq!(summary.simplices_removed_total, 4); - assert_eq!(summary.simplices_removed_max, 4); - - // Borrowed API: caller retains ownership of insertion stats. - assert_eq!(stats.attempts, 3); - assert!(matches!(stats.result, InsertionResult::Inserted)); - } - - #[test] - fn test_construction_statistics_record_insertion_tracks_skipped_variants() { - init_tracing(); - - let mut summary = ConstructionStatistics::default(); - let skipped_duplicate = InsertionStatistics { - attempts: 1, - simplices_removed_during_repair: 0, - result: InsertionResult::SkippedDuplicate, - }; - let skipped_degeneracy = InsertionStatistics { - attempts: 2, - simplices_removed_during_repair: 5, - result: InsertionResult::SkippedDegeneracy, - }; - - summary.record_insertion(&skipped_duplicate); - summary.record_insertion(&skipped_degeneracy); - - assert_eq!(summary.inserted, 0); - assert_eq!(summary.skipped_duplicate, 1); - assert_eq!(summary.skipped_degeneracy, 1); - assert_eq!(summary.total_skipped(), 2); - assert_eq!(summary.total_attempts, 3); - assert_eq!(summary.max_attempts, 2); - assert_eq!(summary.attempts_histogram.get(1).copied().unwrap_or(0), 1); - assert_eq!(summary.attempts_histogram.get(2).copied().unwrap_or(0), 1); - assert_eq!(summary.used_perturbation, 1); - assert_eq!(summary.simplices_removed_total, 5); - assert_eq!(summary.simplices_removed_max, 5); - } - - #[test] - fn test_construction_statistics_record_skip_sample_caps_at_eight_samples() { - init_tracing(); - - let mut summary = ConstructionStatistics::default(); - for index in 0..10 { - let sample_index_u32 = u32::try_from(index).unwrap(); - let coordinate_base = >::from(sample_index_u32); - summary.record_skip_sample(ConstructionSkipSample { - index, - uuid: Uuid::from_u128( - >::from(sample_index_u32) + 1, - ), - coords: vec![ - coordinate_base, - coordinate_base + 0.5, - coordinate_base + 1.0, - ], - coords_available: true, - attempts: index + 1, - error: format!("skip sample #{index}"), - }); - } - - assert_eq!(summary.skip_samples.len(), 8); - assert_eq!(summary.skip_samples.first().map(|s| s.index), Some(0)); - assert_eq!(summary.skip_samples.last().map(|s| s.index), Some(7)); - assert_eq!( - summary.skip_samples.last().map(|s| s.uuid), - Some(Uuid::from_u128(8)) - ); - } - - #[test] - fn test_construction_statistics_records_slowest_insertion_samples() { - init_tracing(); - - let mut summary = ConstructionStatistics::default(); - for index in 0..10 { - let sample_index_u32 = u32::try_from(index).unwrap(); - summary.record_slow_insertion_sample(ConstructionSlowInsertionSample { - index, - uuid: Uuid::from_u128( - >::from(sample_index_u32) + 1, - ), - attempts: 1, - result: InsertionResult::Inserted, - elapsed_nanos: >::from(sample_index_u32) * 1_000, - simplices_after: index, - locate_calls: 1, - locate_walk_steps_total: index, - conflict_region_calls: 1, - conflict_region_simplices_total: index, - cavity_insertion_calls: 1, - global_conflict_scans: 0, - hull_extension_calls: 0, - topology_validation_calls: 1, - }); - } - - assert_eq!(summary.slow_insertions.len(), 8); - assert_eq!(summary.slow_insertions.first().map(|s| s.index), Some(9)); - assert_eq!(summary.slow_insertions.last().map(|s| s.index), Some(2)); - assert!( - summary - .slow_insertions - .windows(2) - .all(|pair| pair[0].elapsed_nanos >= pair[1].elapsed_nanos) - ); - } - - #[test] - fn test_select_balanced_simplex_indices_insufficient_vertices() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0]), - vertex!([0.0, 1.0, 0.0]), - ]; - - let result = select_balanced_simplex_indices(&vertices); - assert!(result.is_none()); - } - - #[test] - fn test_select_balanced_simplex_indices_rejects_non_finite_coords() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0]), - vertex!([0.0, 1.0, 0.0]), - Vertex::new_with_uuid(Point::new([f64::NAN, 0.0, 0.0]), Uuid::new_v4(), None), - ]; - - let result = select_balanced_simplex_indices(&vertices); - assert!(result.is_none()); - } - - macro_rules! max_volume_axis_simplex_test { - ($test_name:ident, $dimension:literal, [$($coords:expr),+ $(,)?], [$($expected_idx:expr),+ $(,)?]) => { - #[test] - fn $test_name() { - init_tracing(); - let vertices: Vec> = vec![$(vertex!($coords)),+]; - - let result = select_max_volume_simplex_indices(&vertices) - .expect("max-volume simplex selection failed"); - let expected_indices = [$($expected_idx),+]; - - assert_eq!(result.len(), expected_indices.len()); - for expected_idx in expected_indices { - assert!( - result.contains(&expected_idx), - "expected selected simplex {result:?} to contain vertex index {expected_idx}" - ); - } - } - }; - } - - max_volume_axis_simplex_test!( - test_select_max_volume_simplex_indices_prefers_largest_triangle_2d, - 2, - [ - [0.0, 0.0], - [1.0, 0.0], - [0.0, 1.0], - [10.0, 0.0], - [0.0, 10.0], - [1.0, 1.0], - ], - [0, 3, 4] - ); - - max_volume_axis_simplex_test!( - test_select_max_volume_simplex_indices_prefers_largest_tetrahedron, - 3, - [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0], - [10.0, 0.0, 0.0], - [0.0, 10.0, 0.0], - [0.0, 0.0, 10.0], - ], - [0, 4, 5, 6] - ); - - max_volume_axis_simplex_test!( - test_select_max_volume_simplex_indices_prefers_largest_simplex_4d, - 4, - [ - [0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0], - [10.0, 0.0, 0.0, 0.0], - [0.0, 10.0, 0.0, 0.0], - [0.0, 0.0, 10.0, 0.0], - [0.0, 0.0, 0.0, 10.0], - ], - [0, 5, 6, 7, 8] - ); - - max_volume_axis_simplex_test!( - test_select_max_volume_simplex_indices_prefers_largest_simplex_5d, - 5, - [ - [0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0], - [10.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 10.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 10.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 10.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 10.0], - ], - [0, 6, 7, 8, 9, 10] - ); - - #[test] - fn test_select_max_volume_simplex_indices_rejects_degenerate_pool() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0]), - vertex!([2.0, 0.0, 0.0]), - vertex!([3.0, 0.0, 0.0]), - ]; - - let result = select_max_volume_simplex_indices(&vertices); - assert!(result.is_none()); - } - - #[test] - fn test_reorder_vertices_for_simplex_valid_and_invalid() { - init_tracing(); - let vertices: Vec> = 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]), - vertex!([2.0, 2.0, 2.0]), - ]; - - let indices = [2_usize, 0, 3, 1]; - let reordered = - reorder_vertices_for_simplex(&vertices, &indices).expect("expected valid reorder"); - - let expected_first: Vec<[f64; 3]> = - indices.iter().map(|&i| (&vertices[i]).into()).collect(); - let actual_first: Vec<[f64; 3]> = reordered.iter().take(4).map(Into::into).collect(); - assert_eq!(actual_first, expected_first); - - let remaining_expected: Vec<[f64; 3]> = vertices - .iter() - .enumerate() - .filter(|(idx, _)| !indices.contains(idx)) - .map(|(_, v)| (*v).into()) - .collect(); - let remaining_actual: Vec<[f64; 3]> = reordered.iter().skip(4).map(Into::into).collect(); - assert_eq!(remaining_actual, remaining_expected); - - assert!(reorder_vertices_for_simplex(&vertices, &[0, 1, 2]).is_none()); - assert!(reorder_vertices_for_simplex(&vertices, &[0, 1, 1, 2]).is_none()); - assert!(reorder_vertices_for_simplex(&vertices, &[0, 1, 2, 99]).is_none()); - } - - #[test] - fn test_preprocess_vertices_for_construction_balanced_sets_fallback() { - init_tracing(); - let vertices: Vec> = 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]), - vertex!([2.0, 2.0, 2.0]), - ]; - - let preprocess = DelaunayTriangulation::, (), (), 3>::preprocess_vertices_for_construction( - &vertices, - DedupPolicy::Off, - InsertionOrderStrategy::Input, - InitialSimplexStrategy::Balanced, - ) - .expect("preprocess failed"); - - assert!(preprocess.fallback_slice().is_some()); - assert_eq!(preprocess.primary_slice(&vertices).len(), vertices.len()); - assert_eq!(preprocess.fallback_slice().unwrap().len(), vertices.len()); - assert!(preprocess.grid_cell_size().is_some()); - } - - #[test] - fn test_preprocess_vertices_for_construction_max_volume_sets_largest_simplex_first() { - init_tracing(); - let vertices: Vec> = 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]), - vertex!([10.0, 0.0, 0.0]), - vertex!([0.0, 10.0, 0.0]), - vertex!([0.0, 0.0, 10.0]), - ]; - - let preprocess = DelaunayTriangulation::< - AdaptiveKernel, - (), - (), - 3, - >::preprocess_vertices_for_construction( - &vertices, - DedupPolicy::Off, - InsertionOrderStrategy::Input, - InitialSimplexStrategy::MaxVolume, - ) - .expect("preprocess failed"); + let ConstructionOptions { + insertion_order, + dedup_policy, + initial_simplex, + retry_policy, + batch_repair_policy, + use_global_repair_fallback, + } = options; - let primary = preprocess.primary_slice(&vertices); - assert!(primary.len() >= 4); - let first_simplex = &primary[..4]; - let first_simplex_contains = |expected_coords: [f64; 3]| { - first_simplex.iter().any(|vertex| { - vertex - .point() - .coords() - .iter() - .zip(expected_coords) - .all(|(actual, expected)| (*actual - expected).abs() <= f64::EPSILON) - }) + let preprocessing_started = Instant::now(); + let preprocessed = match Self::preprocess_vertices_for_construction( + vertices, + dedup_policy, + insertion_order, + initial_simplex, + ) { + Ok(preprocessed) => preprocessed, + Err(error) => { + let mut statistics = ConstructionStatistics::default(); + statistics + .telemetry + .record_construction_preprocessing_timing(duration_nanos_saturating( + preprocessing_started.elapsed(), + )); + return Err(DelaunayTriangulationConstructionErrorWithStatistics { + error, + statistics, + }); + } }; + let preprocessing_nanos = duration_nanos_saturating(preprocessing_started.elapsed()); + let grid_cell_size = preprocessed.grid_cell_size(); + let primary_vertices: &[Vertex] = preprocessed.primary_slice(vertices); + let fallback_vertices = preprocessed.fallback_slice(); - assert!(preprocess.fallback_slice().is_some()); - assert!(first_simplex_contains([0.0, 0.0, 0.0])); - assert!(first_simplex_contains([10.0, 0.0, 0.0])); - assert!(first_simplex_contains([0.0, 10.0, 0.0])); - assert!(first_simplex_contains([0.0, 0.0, 10.0])); - } - - #[test] - fn test_preprocess_vertices_rejects_invalid_epsilon_tolerance() { - init_tracing(); - let vertices: Vec> = 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 result = DelaunayTriangulation::, (), (), 3>::preprocess_vertices_for_construction( - &vertices, - DedupPolicy::Epsilon { tolerance: -1.0 }, - InsertionOrderStrategy::Input, - InitialSimplexStrategy::First, - ); - - assert!(matches!( - result, - Err(DelaunayTriangulationConstructionError::Triangulation( - DelaunayConstructionFailure::GeometricDegeneracy { .. } - )) - )); - } - - #[test] - fn stats_preprocess_error_defaults() { - init_tracing(); - let vertices: Vec> = 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 options = ConstructionOptions::default().with_dedup_policy(DedupPolicy::Epsilon { - tolerance: f64::NAN, - }); + let build_with_vertices = |vertices: &[Vertex]| { + match retry_policy { + RetryPolicy::Disabled => {} + RetryPolicy::Shuffled { + attempts, + base_seed, + } => { + if Self::should_retry_construction(vertices) { + return Self::build_with_shuffled_retries_with_construction_statistics( + kernel, + vertices, + topology_guarantee, + attempts, + base_seed, + grid_cell_size, + batch_repair_policy, + use_global_repair_fallback, + ); + } + } + RetryPolicy::DebugOnlyShuffled { + attempts, + base_seed, + } => { + if cfg!(any(test, debug_assertions)) + && Self::should_retry_construction(vertices) + { + return Self::build_with_shuffled_retries_with_construction_statistics( + kernel, + vertices, + topology_guarantee, + attempts, + base_seed, + grid_cell_size, + batch_repair_policy, + use_global_repair_fallback, + ); + } + } + } - let error = - DelaunayTriangulation::, (), (), 3>::with_options_and_statistics( - &AdaptiveKernel::new(), - &vertices, - TopologyGuarantee::PLManifold, - options, + Self::build_with_kernel_inner_with_construction_statistics( + ::clone(kernel), + vertices, + topology_guarantee, + grid_cell_size, + batch_repair_policy, + use_global_repair_fallback, ) - .expect_err("NaN epsilon should fail during preprocessing"); + }; - assert_eq!(error.statistics.inserted, 0); - assert_eq!(error.statistics.total_skipped(), 0); - assert_eq!(error.statistics.total_attempts, 0); - assert!(error.statistics.skip_samples.is_empty()); - assert!(matches!( - error.error, - DelaunayTriangulationConstructionError::Triangulation(_) - )); - } + match build_with_vertices(primary_vertices) { + Ok((dt, mut stats)) => { + stats + .telemetry + .record_construction_preprocessing_timing(preprocessing_nanos); + Ok((dt, stats)) + } + Err(mut primary_err) => { + let Some(fallback) = fallback_vertices else { + primary_err + .statistics + .telemetry + .record_construction_preprocessing_timing(preprocessing_nanos); + return Err(primary_err); + }; - fn vertices_from_coords_permutation_3d( - coords: &[[f64; 3]], - permutation: &[usize], - ) -> Vec> { - permutation.iter().map(|&i| vertex!(coords[i])).collect() + match build_with_vertices(fallback) { + Ok((dt, stats)) => { + let mut aggregate = primary_err.statistics; + aggregate.merge_from(&stats); + aggregate + .telemetry + .record_construction_preprocessing_timing(preprocessing_nanos); + Ok((dt, aggregate)) + } + Err(fallback_err) => { + let mut aggregate = primary_err.statistics; + aggregate.merge_from(&fallback_err.statistics); + aggregate + .telemetry + .record_construction_preprocessing_timing(preprocessing_nanos); + Err(DelaunayTriangulationConstructionErrorWithStatistics { + error: fallback_err.error, + statistics: aggregate, + }) + } + } + } + } } - #[test] - fn test_bulk_construction_skips_near_duplicate_coordinates_3d() { - init_tracing(); - // Test that epsilon-based deduplication removes near-duplicates - let vertices: Vec> = 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]), - vertex!([0.25, 0.25, 0.25]), - // Near-duplicate within tolerance 1e-10 - vertex!([0.25 + 5e-11, 0.25, 0.25]), - ]; - - let opts = ConstructionOptions::default() - .with_dedup_policy(DedupPolicy::Epsilon { tolerance: 1e-10 }) - .with_retry_policy(RetryPolicy::Disabled); - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new_with_options(&vertices, opts).unwrap(); - - assert_eq!(dt.number_of_vertices(), 5); - assert!(dt.validate().is_ok()); - } + /// Applies deduplication, insertion ordering, and initial-simplex selection + /// before any topology is created. + pub(crate) fn preprocess_vertices_for_construction( + vertices: &[Vertex], + dedup_policy: DedupPolicy, + insertion_order: InsertionOrderStrategy, + initial_simplex: InitialSimplexStrategy, + ) -> PreprocessVerticesResult { + let default_tolerance = default_duplicate_tolerance::(); - fn coord_sequence_3d(vertices: &[Vertex]) -> Vec<[f64; 3]> { - vertices.iter().map(Into::into).collect() - } + let mut epsilon: Option = None; + if let DedupPolicy::Epsilon { tolerance } = dedup_policy { + if !tolerance.is_finite() || tolerance < 0.0 { + return Err(TriangulationConstructionError::GeometricDegeneracy { + message: format!( + "Invalid DedupPolicy::Epsilon tolerance {tolerance:?} (must be finite and non-negative)" + ), + } + .into()); + } - #[test] - fn test_insertion_order_hilbert_is_deterministic_across_permutations_3d() { - init_tracing(); - let coords: [[f64; 3]; 8] = [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0], - [1.0, 1.0, 1.0], - [2.0, 0.0, 1.0], - [-1.0, 5.0, 0.0], - [3.0, 2.0, 1.0], - ]; + let Some(epsilon_value) = ::from(tolerance) else { + return Err(TriangulationConstructionError::GeometricDegeneracy { + message: format!( + "Failed to convert DedupPolicy::Epsilon tolerance {tolerance:?} into scalar type" + ), + } + .into()); + }; + epsilon = Some(epsilon_value); + } - let permutations: [&[usize]; 4] = [ - &[0, 1, 2, 3, 4, 5, 6, 7], - &[7, 6, 5, 4, 3, 2, 1, 0], - &[2, 3, 4, 5, 6, 7, 0, 1], - &[1, 3, 5, 7, 0, 2, 4, 6], - ]; + let grid_cell_size_value = + if let (DedupPolicy::Epsilon { .. }, Some(eps)) = (dedup_policy, epsilon) { + if eps > K::Scalar::zero() { + eps + } else { + default_tolerance + } + } else { + default_tolerance + }; + let mut grid: HashGridIndex = HashGridIndex::new(grid_cell_size_value); - // Test both dedup_quantized=false (sort-only) and dedup_quantized=true - // (the real code path used by order_vertices_by_strategy). - let expected_no_dedup = vertices_from_coords_permutation_3d(&coords, permutations[0]); - let expected_no_dedup = - coord_sequence_3d(&order_vertices_hilbert(expected_no_dedup, false)); + // Deduplicate first to reduce work for ordering strategies. + let mut owned_vertices: Option>> = match dedup_policy { + DedupPolicy::Off => None, + DedupPolicy::Exact => { + let vertices = vertices.to_vec(); + if hash_grid_usable_for_vertices(&grid, &vertices) { + Some(dedup_vertices_exact_hash_grid(vertices, &mut grid)) + } else { + Some(dedup_vertices_exact_sorted(vertices)) + } + } + DedupPolicy::Epsilon { .. } => { + let epsilon = epsilon.expect("epsilon validated above"); + let vertices = vertices.to_vec(); + if hash_grid_usable_for_vertices(&grid, &vertices) { + Some(dedup_vertices_epsilon_hash_grid( + vertices, epsilon, &mut grid, + )) + } else { + Some(dedup_vertices_epsilon_quantized(vertices, epsilon)) + } + } + }; - let expected_dedup = vertices_from_coords_permutation_3d(&coords, permutations[0]); - let expected_dedup = coord_sequence_3d(&order_vertices_hilbert(expected_dedup, true)); + owned_vertices = match insertion_order { + InsertionOrderStrategy::Input => owned_vertices, + _ => Some(order_vertices_by_strategy( + owned_vertices.unwrap_or_else(|| vertices.to_vec()), + insertion_order, + )), + }; - for perm in &permutations[1..] { - let vertices = vertices_from_coords_permutation_3d(&coords, perm); - let got = coord_sequence_3d(&order_vertices_hilbert(vertices, false)); - assert_eq!(got, expected_no_dedup); + let (primary, fallback) = match initial_simplex { + InitialSimplexStrategy::First => (owned_vertices, None), + InitialSimplexStrategy::Balanced => { + let base = owned_vertices.unwrap_or_else(|| vertices.to_vec()); + if let Some(indices) = select_balanced_simplex_indices(&base) { + if let Some(reordered) = reorder_vertices_for_simplex(&base, &indices) { + (Some(reordered), Some(base)) + } else { + (Some(base), None) + } + } else { + (Some(base), None) + } + } + InitialSimplexStrategy::MaxVolume => { + let base = owned_vertices.unwrap_or_else(|| vertices.to_vec()); + if let Some(indices) = select_max_volume_simplex_indices(&base) { + if let Some(reordered) = reorder_vertices_for_simplex(&base, &indices) { + (Some(reordered), Some(base)) + } else { + (Some(base), None) + } + } else { + (Some(base), None) + } + } + }; - let vertices = vertices_from_coords_permutation_3d(&coords, perm); - let got = coord_sequence_3d(&order_vertices_hilbert(vertices, true)); - assert_eq!(got, expected_dedup); - } - } + let final_slice = primary.as_deref().unwrap_or(vertices); + let grid_cell_size = if hash_grid_usable_for_vertices(&grid, final_slice) { + Some(grid.cell_size()) + } else { + None + }; - // ========================================================================= - // HILBERT DEDUP — GENERIC HELPERS - // ========================================================================= + Ok(PreprocessVertices { + primary, + fallback, + grid_cell_size, + }) + } - /// Build D+1 standard simplex vertices: origin + D unit vectors. - fn simplex_vertices() -> Vec> { - let mut verts = Vec::with_capacity(D + 1); - verts.push(vertex!([0.0; D])); - for i in 0..D { - let mut coords = [0.0; D]; - coords[i] = 1.0; - verts.push(vertex!(coords)); - } - verts + /// Returns `true` if the construction error is deterministic and should not + /// be masked by shuffled retry logic (e.g. duplicate UUIDs, internal bugs). + pub(crate) const fn is_non_retryable_construction_error( + err: &DelaunayTriangulationConstructionError, + ) -> bool { + matches!( + err, + DelaunayTriangulationConstructionError::Triangulation( + DelaunayConstructionFailure::Tds { + reason: TdsConstructionFailure::DuplicateUuid { .. } + | TdsConstructionFailure::Validation { .. }, + } | DelaunayConstructionFailure::InternalInconsistency { .. } + | DelaunayConstructionFailure::DelaunayRepair { .. } + | DelaunayConstructionFailure::InsertionTopologyValidation { .. } + | DelaunayConstructionFailure::FinalTopologyValidation { .. }, + ) + ) } - /// Build simplex vertices plus exact duplicates of the first two. - fn simplex_with_duplicates() -> (Vec>, usize) { - let mut verts = simplex_vertices::(); - let distinct = verts.len(); - // Duplicate the origin and first unit vector - verts.push(vertex!([0.0; D])); - let mut unit = [0.0; D]; - unit[0] = 1.0; - verts.push(vertex!(unit)); - (verts, distinct) + /// Identifies D≥4 local-repair failures that can safely try escalation and + /// then enter the bounded soft-fail path. + pub(crate) const fn can_soft_fail(repair_err: &DelaunayRepairError) -> bool { + matches!( + repair_err, + DelaunayRepairError::NonConvergent { .. } + | DelaunayRepairError::PostconditionFailed { .. } + ) } - /// Build simplex vertices plus an interior point (all distinct). - fn simplex_with_interior() -> Vec> { - let mut verts = simplex_vertices::(); - let dimension = safe_usize_to_scalar::(D).expect("test dimensions fit in f64"); - let interior = [0.1_f64 / dimension; D]; - verts.push(vertex!(interior)); - verts + /// Converts non-soft-fail local-repair errors into construction failures so + /// the bulk loop does not canonicalize or keep mutating after unexpected + /// topology/flip failures. + pub(crate) fn map_hard_repair_error( + index: usize, + repair_err: DelaunayRepairError, + ) -> DelaunayTriangulationConstructionError { + let message = + format!("per-insertion Delaunay repair failed at index {index}: {repair_err}"); + if is_geometric_repair_error(&repair_err) { + TriangulationConstructionError::GeometricDegeneracy { message }.into() + } else { + DelaunayTriangulationConstructionError::Triangulation( + DelaunayConstructionFailure::DelaunayRepair { + phase: DelaunayConstructionRepairPhase::BatchLocal { index }, + source: Box::new(repair_err), + }, + ) + } } - // ========================================================================= - // HILBERT DEDUP — MACRO-GENERATED PER-DIMENSION TESTS (2D–5D) - // ========================================================================= + pub(crate) fn map_completion_repair_error( + message: String, + repair_error: DelaunayRepairError, + ) -> DelaunayTriangulationConstructionError { + if is_geometric_repair_error(&repair_error) { + TriangulationConstructionError::GeometricDegeneracy { message }.into() + } else { + DelaunayTriangulationConstructionError::Triangulation( + DelaunayConstructionFailure::DelaunayRepair { + phase: DelaunayConstructionRepairPhase::Completion, + source: Box::new(repair_error), + }, + ) + } + } - /// Generate Hilbert-sort dedup tests for a given dimension: + /// Map an [`InsertionError`] from post-construction orientation canonicalization + /// into a [`TriangulationConstructionError`]. /// - /// - exact duplicates are removed - /// - distinct points are preserved - /// - all-identical inputs collapse to 1 - macro_rules! gen_hilbert_dedup_tests { - ($dim:literal) => { - pastey::paste! { - #[test] - fn []() { - init_tracing(); - let (vertices, distinct) = simplex_with_duplicates::<$dim>(); - assert!(vertices.len() > distinct); - let result = order_vertices_hilbert(vertices, true); - assert_eq!( - result.len(), - distinct, - "{}D: exact duplicates should be removed", - $dim - ); + /// Structural / data-structure errors (missing simplices, broken invariants) become + /// [`InternalInconsistency`](TriangulationConstructionError::InternalInconsistency) + /// because they indicate algorithmic bugs rather than bad input geometry. + /// Geometry-related failures (degenerate predicates, conflict regions, etc.) become + /// [`GeometricDegeneracy`](TriangulationConstructionError::GeometricDegeneracy). + /// + /// NOTE: This match is intentionally exhaustive over `InsertionError`. + /// When adding new variants, decide whether the failure mode is an internal + /// bug or an input-geometry problem. + pub(crate) fn map_orientation_canonicalization_error( + error: InsertionError, + ) -> TriangulationConstructionError { + match error { + // Geometric orientation errors (degenerate or negative) are + // geometry problems, not internal bugs. + InsertionError::TopologyValidation(error @ TdsError::Geometric(_)) => { + TriangulationConstructionError::GeometricDegeneracy { + message: format!( + "Failed to canonicalize orientation after post-construction repair: {error}" + ), } - - #[test] - fn []() { - init_tracing(); - let vertices = simplex_with_interior::<$dim>(); - let expected = vertices.len(); - let result = order_vertices_hilbert(vertices, true); - assert_eq!( - result.len(), - expected, - "{}D: distinct points should all be preserved", - $dim - ); + } + // Structural / data-structure errors indicate algorithmic bugs, + // not input-geometry problems. + // + // NOTE: OrientationViolation (coherent-orientation invariant breach between + // adjacent simplices) lands here rather than in the geometry arm above. After + // normalize_coherent_orientation() BFS, a surviving violation would mean the + // normalization algorithm failed its post-condition — an internal bug, not + // bad input geometry. DegenerateOrientation / NegativeOrientation capture + // the actual FP-related geometry failures. + error @ (InsertionError::TopologyValidation(_) + | InsertionError::TopologyValidationFailed { .. } + | InsertionError::CavityFilling { .. } + | InsertionError::NeighborWiring { .. } + | InsertionError::DuplicateUuid { .. }) => { + TriangulationConstructionError::InternalInconsistency { + message: format!( + "Failed to canonicalize orientation after post-construction repair: {error}" + ), } - - #[test] - fn []() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.5; $dim]), - vertex!([0.5; $dim]), - vertex!([0.5; $dim]), - ]; - let result = order_vertices_hilbert(vertices, true); - assert_eq!( - result.len(), - 1, - "{}D: all-identical inputs should collapse to 1", - $dim - ); + } + InsertionError::DelaunayRepairFailed { source, context } => { + let message = format!( + "Failed to canonicalize orientation after post-construction repair: \ + Delaunay repair failed ({context}): {source}" + ); + if is_geometric_repair_error(&source) { + TriangulationConstructionError::GeometricDegeneracy { message } + } else { + TriangulationConstructionError::InternalInconsistency { message } } } - }; + // Geometry-related failures (degenerate input, predicate issues). + error @ (InsertionError::ConflictRegion(_) + | InsertionError::Location(_) + | InsertionError::NonManifoldTopology { .. } + | InsertionError::HullExtension { .. } + | InsertionError::DelaunayValidationFailed { .. } + | InsertionError::DuplicateCoordinates { .. }) => { + TriangulationConstructionError::GeometricDegeneracy { + message: format!( + "Failed to canonicalize orientation after post-construction repair: {error}" + ), + } + } + } } - gen_hilbert_dedup_tests!(2); - gen_hilbert_dedup_tests!(3); - gen_hilbert_dedup_tests!(4); - gen_hilbert_dedup_tests!(5); + /// Classifies insertion-layer failures as input degeneracy or internal + /// inconsistency for construction callers. + pub(crate) fn map_insertion_error(error: InsertionError) -> TriangulationConstructionError { + match error { + InsertionError::CavityFilling { reason } => { + TriangulationConstructionError::InsertionCavityFilling { source: reason } + } + InsertionError::NeighborWiring { reason } => { + TriangulationConstructionError::InternalInconsistency { + message: format!("Neighbor wiring failed: {reason}"), + } + } + InsertionError::TopologyValidation(source) => { + TriangulationConstructionError::from(TdsConstructionError::ValidationError(source)) + } + InsertionError::DuplicateUuid { entity, uuid } => { + TriangulationConstructionError::from(TdsConstructionError::DuplicateUuid { + entity, + uuid, + }) + } + InsertionError::DuplicateCoordinates { coordinates } => { + TriangulationConstructionError::DuplicateCoordinates { coordinates } + } + InsertionError::DelaunayRepairFailed { source, context } => { + let message = format!("Delaunay repair failed ({context}): {source}"); + if is_geometric_repair_error(&source) { + TriangulationConstructionError::GeometricDegeneracy { message } + } else { + TriangulationConstructionError::InternalInconsistency { message } + } + } - // ========================================================================= - // HILBERT DEDUP — STANDALONE EDGE-CASE TESTS - // ========================================================================= + InsertionError::ConflictRegion(source) => { + TriangulationConstructionError::InsertionConflictRegion { source } + } + InsertionError::Location(source) => { + TriangulationConstructionError::InsertionLocation { source } + } + InsertionError::NonManifoldTopology { + facet_hash, + simplex_count, + } => TriangulationConstructionError::InsertionNonManifoldTopology { + facet_hash, + simplex_count, + }, + InsertionError::HullExtension { reason } => { + TriangulationConstructionError::InsertionHullExtension { reason } + } + InsertionError::DelaunayValidationFailed { source } => { + TriangulationConstructionError::InsertionDelaunayValidation { source } + } + InsertionError::TopologyValidationFailed { message, source } => { + TriangulationConstructionError::InsertionTopologyValidation { message, source } + } + } + } - #[test] - fn test_hilbert_dedup_empty_input() { - let vertices: Vec> = vec![]; - let result = order_vertices_hilbert(vertices, true); - assert!(result.is_empty(), "empty input must produce empty output"); + /// Avoids retry work when construction has no incremental phase to reorder. + pub(crate) const fn should_retry_construction(vertices: &[Vertex]) -> bool { + D >= 2 && vertices.len() > D + 1 } - #[test] - fn test_hilbert_dedup_single_vertex() { - let vertices: Vec> = vec![vertex!([1.0, 2.0, 3.0])]; - let result = order_vertices_hilbert(vertices, true); - assert_eq!(result.len(), 1, "single vertex must be preserved"); + /// Derives an input-order-independent seed so shuffled retries are + /// reproducible for the same vertex set. + pub(crate) fn construction_shuffle_seed(vertices: &[Vertex]) -> u64 { + let mut vertex_hashes = Vec::with_capacity(vertices.len()); + for vertex in vertices { + let mut hasher = FastHasher::default(); + vertex.hash(&mut hasher); + vertex_hashes.push(hasher.finish()); + } + vertex_hashes.sort_unstable(); + stable_hash_u64_slice(&vertex_hashes) } - #[test] - fn test_hilbert_dedup_already_unique() { - // Distinct vertices — dedup should be a no-op. - let vertices: Vec> = 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 n = vertices.len(); - let result = order_vertices_hilbert(vertices, true); - assert_eq!(result.len(), n, "already-unique input must be unchanged"); + /// Keeps construction retry shuffling deterministic for diagnostics and tests. + pub(crate) fn shuffle_vertices(vertices: &mut [Vertex], seed: u64) { + let mut rng = StdRng::seed_from_u64(seed); + vertices.shuffle(&mut rng); } +} - #[test] - fn test_new_with_options_hilbert_smoke_3d() { - init_tracing(); - let vertices: Vec> = 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]), - vertex!([0.25, 0.25, 0.25]), - ]; +/// Reads the optional batch-construction progress cadence from the environment. +/// +/// `DELAUNAY_BULK_PROGRESS_EVERY` is the canonical knob. The large-scale debug +/// harness also reuses `DELAUNAY_LARGE_DEBUG_PROGRESS_EVERY` so manual runs can +/// request periodic progress without additional wiring. +pub(crate) fn bulk_progress_every_from_env() -> Option { + [ + "DELAUNAY_BULK_PROGRESS_EVERY", + "DELAUNAY_LARGE_DEBUG_PROGRESS_EVERY", + ] + .into_iter() + .find_map(|name| { + env::var(name) + .ok() + .and_then(|raw| raw.trim().parse::().ok()) + }) + .filter(|every| *every > 0) +} - let opts = ConstructionOptions::default() - .with_insertion_order(InsertionOrderStrategy::Hilbert) - .with_retry_policy(RetryPolicy::Disabled); +/// Converts a measured duration to nanoseconds while saturating pathological +/// values that exceed the public telemetry counter width. +pub(crate) fn duration_nanos_saturating(duration: Duration) -> u64 { + u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX) +} - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new_with_options(&vertices, opts).unwrap(); +/// Snapshot of one batch-construction progress sample. +#[derive(Clone, Copy, Debug)] +pub(crate) struct BatchProgressSample { + pub(crate) bulk_processed: usize, + pub(crate) bulk_inserted: usize, + pub(crate) bulk_skipped: usize, + pub(crate) simplex_count: usize, + pub(crate) perturbation_seed: u64, +} - assert_eq!(dt.number_of_vertices(), 5); - assert!(dt.validate().is_ok()); +/// Rolling state used to compute periodic batch throughput summaries. +#[derive(Clone, Copy, Debug)] +pub(crate) struct BatchProgressState { + pub(crate) input_vertices: usize, + pub(crate) initial_simplex_vertices: usize, + pub(crate) bulk_vertices: usize, + pub(crate) progress_every: usize, + pub(crate) started: Instant, + pub(crate) last_progress: Instant, + pub(crate) last_processed: usize, +} + +/// Emits periodic batch-construction progress for long-running release-mode +/// investigations such as the 4D large-scale debug harness. +/// +/// Progress is emitted via `tracing::debug!`; enable with `RUST_LOG=debug` (the +/// large-scale debug harness wires this up automatically when +/// `DELAUNAY_BULK_PROGRESS_EVERY` is set). +pub(crate) fn log_bulk_progress_if_due( + sample: BatchProgressSample, + state: &mut Option, +) { + let Some(state) = state.as_mut() else { + return; + }; + if sample.bulk_processed == 0 { + return; } - #[test] - fn test_new_with_options_shuffled_retry_policy_smoke_3d() { - init_tracing(); - let vertices: Vec> = 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]), - vertex!([0.25, 0.25, 0.25]), - ]; + // Always log the final sample, even when the total is not an exact multiple of the + // requested cadence, so interrupted runs still end with a complete progress line. + let should_log = sample.bulk_processed == state.bulk_vertices + || sample.bulk_processed.is_multiple_of(state.progress_every); + if !should_log { + return; + } - let opts = ConstructionOptions::default() - .with_insertion_order(InsertionOrderStrategy::Input) - .with_retry_policy(RetryPolicy::Shuffled { - attempts: NonZeroUsize::new(2).unwrap(), - base_seed: Some(123), - }); + let elapsed = state.started.elapsed(); + let chunk_elapsed = state.last_progress.elapsed(); + let chunk_processed = sample.bulk_processed.saturating_sub(state.last_processed); - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new_with_options(&vertices, opts).unwrap(); + let overall_rate = safe_usize_to_scalar::(sample.bulk_processed) + .ok() + .map(|processed| processed / elapsed.as_secs_f64().max(1e-9)); + let chunk_rate = safe_usize_to_scalar::(chunk_processed) + .ok() + .map(|processed| processed / chunk_elapsed.as_secs_f64().max(1e-9)); + + tracing::debug!( + target: "delaunay::bulk_progress", + perturbation_seed = format_args!("0x{:X}", sample.perturbation_seed), + input_vertices = state.input_vertices, + initial_simplex_vertices = state.initial_simplex_vertices, + bulk_processed = sample.bulk_processed, + bulk_vertices = state.bulk_vertices, + bulk_inserted = sample.bulk_inserted, + bulk_skipped = sample.bulk_skipped, + simplices = sample.simplex_count, + elapsed = ?elapsed, + total_rate_pts_per_s = ?overall_rate, + recent_rate_pts_per_s = ?chunk_rate, + "bulk-construction progress" + ); + + state.last_progress = Instant::now(); + state.last_processed = sample.bulk_processed; +} - assert_eq!(dt.number_of_vertices(), 5); - assert!(dt.validate().is_ok()); +/// Emits retry-boundary events for release-mode large-scale construction runs. +pub(crate) fn log_construction_retry_start( + attempt: usize, + attempt_seed: u64, + perturbation_seed: u64, +) { + if !construction_retry_trace_enabled() { + return; } - #[test] - fn test_delaunay_constructors_default_to_pl_manifold_mode() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - - let dt_new: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - assert_eq!(dt_new.topology_guarantee(), TopologyGuarantee::PLManifold); + tracing::debug!( + target: "delaunay::bulk_retry", + attempt, + attempt_seed = format_args!("0x{:X}", attempt_seed), + perturbation_seed = format_args!("0x{:X}", perturbation_seed), + "shuffled retry attempt starting" + ); +} - let dt_empty: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); - assert_eq!(dt_empty.topology_guarantee(), TopologyGuarantee::PLManifold); +/// Emits retry attempt outcomes with optional construction statistics. +pub(crate) fn log_construction_retry_result( + attempt: usize, + attempt_seed: Option, + perturbation_seed: u64, + outcome: &'static str, + error: Option<&str>, + stats: Option<&ConstructionStatistics>, +) { + if !construction_retry_trace_enabled() { + return; + } - let dt_with_kernel: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices).unwrap(); + let attempt_seed_display = + attempt_seed.map_or_else(|| String::from("input-order"), |seed| format!("0x{seed:X}")); + let error_display = error.unwrap_or("-"); - assert_eq!( - dt_with_kernel.topology_guarantee(), - TopologyGuarantee::PLManifold + if let Some(stats) = stats { + tracing::debug!( + target: "delaunay::bulk_retry", + attempt, + attempt_seed = %attempt_seed_display, + perturbation_seed = format_args!("0x{:X}", perturbation_seed), + outcome, + inserted = stats.inserted, + skipped_duplicate = stats.skipped_duplicate, + skipped_degeneracy = stats.skipped_degeneracy, + total_attempts = stats.total_attempts, + max_attempts = stats.max_attempts, + simplices_removed_total = stats.simplices_removed_total, + simplices_removed_max = stats.simplices_removed_max, + error = %error_display, + "shuffled retry attempt result (with stats)" ); - - let dt_from_tds: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::try_from_tds(dt_new.tds().clone(), FastKernel::new()).unwrap(); - assert_eq!( - dt_from_tds.topology_guarantee(), - TopologyGuarantee::PLManifold + } else { + tracing::debug!( + target: "delaunay::bulk_retry", + attempt, + attempt_seed = %attempt_seed_display, + perturbation_seed = format_args!("0x{:X}", perturbation_seed), + outcome, + error = %error_display, + "shuffled retry attempt result" ); } +} - #[test] - fn test_set_topology_guarantee_updates_underlying_triangulation() { - init_tracing(); - let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); +/// Enables release-visible retry-boundary tracing for bulk construction. +fn construction_retry_trace_enabled() -> bool { + bulk_progress_every_from_env().is_some() + || env::var_os("DELAUNAY_DEBUG_SHUFFLE").is_some() + || env::var_os("DELAUNAY_INSERT_TRACE").is_some() +} - assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); - assert_eq!(dt.tri.topology_guarantee, TopologyGuarantee::PLManifold); +#[cfg(test)] +mod tests { + use super::*; + use crate::core::algorithms::flips::{ + DelaunayRepairDiagnostics, DelaunayRepairVerificationContext, FlipContextError, + FlipPredicateError, FlipPredicateOperation, RepairQueueOrder, + }; + use crate::core::algorithms::incremental_insertion::{ + DelaunayRepairFailureContext, HullExtensionReason, NeighborWiringError, + }; + use crate::core::algorithms::locate::{ConflictError, LocateError}; + use crate::core::tds::{EntityKind, GeometricError, InvariantError, SimplexKey, VertexKey}; + use crate::core::validation::TopologyGuarantee; + use crate::core::validation::TriangulationValidationError; + use crate::core::vertex::VertexBuilder; + use crate::diagnostics::BatchLocalRepairTrigger; + use crate::geometry::kernel::{AdaptiveKernel, FastKernel, RobustKernel}; + use crate::geometry::point::Point; + use crate::geometry::traits::coordinate::CoordinateConversionError; + use crate::repair::DelaunayRepairPolicy; + use crate::topology::characteristics::euler::TopologyClassification; + use crate::validation::DelaunayTriangulationValidationError; + use crate::vertex; + use slotmap::KeyData; + use std::num::NonZeroUsize; + use std::sync::Once; + use std::time::Instant; + use uuid::Uuid; + + type TestDelaunay = DelaunayTriangulation, (), (), D>; - dt.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); - assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); - assert_eq!(dt.tri.topology_guarantee, TopologyGuarantee::Pseudomanifold); + fn init_tracing() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_test_writer() + .try_init(); + }); } - #[test] - fn test_new_with_topology_guarantee_sets_pl() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; + struct ForceRepairNonconvergentGuard { + prior: bool, + } - let dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new_with_topology_guarantee( - &vertices, - TopologyGuarantee::PLManifold, - ) - .unwrap(); + impl ForceRepairNonconvergentGuard { + fn enable() -> Self { + let prior = test_hooks::set_force_repair_nonconvergent(true); + Self { prior } + } + } - assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); + impl Drop for ForceRepairNonconvergentGuard { + fn drop(&mut self) { + test_hooks::restore_force_repair_nonconvergent(self.prior); + } } #[test] @@ -9745,1874 +4736,1551 @@ mod tests { )); } - #[test] - fn test_delaunay_check_policy_should_check() { - init_tracing(); - assert!(!DelaunayCheckPolicy::EndOnly.should_check(1)); + macro_rules! test_incremental_insertion { + ($dim:expr, [$($simplex_coords:expr),+ $(,)?], $interior_point:expr) => { + pastey::paste! { + #[test] + fn []() { + init_tracing(); + let mut vertices: Vec> = vec![ + $(vertex!($simplex_coords)),+ + ]; + vertices.push(vertex!($interior_point)); - let every_2 = DelaunayCheckPolicy::EveryN(NonZeroUsize::new(2).unwrap()); - assert!(!every_2.should_check(1)); - assert!(every_2.should_check(2)); - assert!(!every_2.should_check(3)); - assert!(every_2.should_check(4)); - } + let expected_vertices = vertices.len(); + let dt: DelaunayTriangulation<_, (), (), $dim> = + DelaunayTriangulation::new(&vertices).unwrap(); - #[test] - fn test_set_delaunay_check_policy_updates_state() { - init_tracing(); - let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); - assert_eq!(dt.delaunay_check_policy(), DelaunayCheckPolicy::EndOnly); + assert_eq!(dt.number_of_vertices(), expected_vertices); + assert!(dt.number_of_simplices() > 1); + } - let policy = DelaunayCheckPolicy::EveryN(NonZeroUsize::new(3).unwrap()); - dt.set_delaunay_check_policy(policy); - assert_eq!(dt.delaunay_check_policy(), policy); - } + #[test] + fn []() { + init_tracing(); + let mut dt: DelaunayTriangulation<_, (), (), $dim> = + DelaunayTriangulation::empty(); + assert_eq!(dt.number_of_vertices(), 0); + assert_eq!(dt.number_of_simplices(), 0); - // ========================================================================= - // Delaunay repair helper methods - // ========================================================================= + let vertices = vec![$(vertex!($simplex_coords)),+]; + assert_eq!(vertices.len(), $dim + 1); - #[test] - fn test_should_run_delaunay_repair_for_skips_for_dimension_lt_2() { - init_tracing(); - let vertices: Vec> = vec![vertex!([0.0]), vertex!([1.0])]; - let dt: DelaunayTriangulation<_, (), (), 1> = - DelaunayTriangulation::new(&vertices).unwrap(); + for (i, vertex) in vertices.iter().take($dim).enumerate() { + dt.insert(*vertex).unwrap(); + assert_eq!(dt.number_of_vertices(), i + 1); + assert_eq!(dt.number_of_simplices(), 0); + } - assert_eq!(dt.number_of_simplices(), 1); - assert_eq!( - dt.delaunay_repair_policy(), - DelaunayRepairPolicy::EveryInsertion - ); - assert!(!dt.should_run_delaunay_repair_for(dt.topology_guarantee(), 1)); - } + dt.insert(*vertices.last().unwrap()).unwrap(); + assert_eq!(dt.number_of_vertices(), $dim + 1); + assert_eq!(dt.number_of_simplices(), 1); + assert!(dt.is_valid().is_ok()); + } - #[test] - fn test_should_run_delaunay_repair_for_skips_when_no_simplices() { - init_tracing(); - let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); + #[test] + fn []() { + init_tracing(); + let mut dt: DelaunayTriangulation<_, (), (), $dim> = + DelaunayTriangulation::empty(); + let initial_vertices = vec![$(vertex!($simplex_coords)),+]; - assert_eq!(dt.number_of_simplices(), 0); - assert_eq!( - dt.delaunay_repair_policy(), - DelaunayRepairPolicy::EveryInsertion - ); - assert!(!dt.should_run_delaunay_repair_for(dt.topology_guarantee(), 1)); + for vertex in &initial_vertices { + dt.insert(*vertex).unwrap(); + } + assert_eq!(dt.number_of_simplices(), 1); + + dt.insert(vertex!($interior_point)).unwrap(); + assert_eq!(dt.number_of_vertices(), $dim + 2); + assert!(dt.number_of_simplices() > 1); + assert!(dt.is_valid().is_ok()); + } + + #[test] + fn []() { + init_tracing(); + let vertices = vec![$(vertex!($simplex_coords)),+]; + + let mut dt_bootstrap: DelaunayTriangulation<_, (), (), $dim> = + DelaunayTriangulation::empty(); + for vertex in &vertices { + dt_bootstrap.insert(*vertex).unwrap(); + } + + let dt_batch: DelaunayTriangulation<_, (), (), $dim> = + DelaunayTriangulation::new(&vertices).unwrap(); + + assert_eq!(dt_bootstrap.number_of_vertices(), dt_batch.number_of_vertices()); + assert_eq!( + dt_bootstrap.number_of_simplices(), + dt_batch.number_of_simplices() + ); + assert!(dt_bootstrap.is_valid().is_ok()); + assert!(dt_batch.is_valid().is_ok()); + } + } + }; } + test_incremental_insertion!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], [0.5, 0.5]); + + test_incremental_insertion!( + 3, + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0] + ], + [0.2, 0.2, 0.2] + ); + + test_incremental_insertion!( + 4, + [ + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0] + ], + [0.2, 0.2, 0.2, 0.2] + ); + + test_incremental_insertion!( + 5, + [ + [0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0] + ], + [0.2, 0.2, 0.2, 0.2, 0.2] + ); + #[test] - fn test_should_run_delaunay_repair_for_skips_when_policy_never() { + fn test_with_kernel_fast_kernel() { init_tracing(); - let vertices: Vec> = vec![ + let vertices = vec![ vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); + let dt: DelaunayTriangulation, (), (), 2> = + DelaunayTriangulation::with_kernel(&FastKernel::new(), &vertices).unwrap(); + + assert_eq!(dt.number_of_vertices(), 3); assert_eq!(dt.number_of_simplices(), 1); - dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); - assert!(!dt.should_run_delaunay_repair_for(dt.topology_guarantee(), 1)); } #[test] - fn test_should_run_delaunay_repair_for_respects_every_n_schedule() { + fn test_with_kernel_robust_kernel() { init_tracing(); - let vertices: Vec> = vec![ + let vertices = vec![ vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - dt.set_delaunay_repair_policy(DelaunayRepairPolicy::EveryN(NonZeroUsize::new(2).unwrap())); - let topology = dt.topology_guarantee(); + let dt: DelaunayTriangulation, (), (), 2> = + DelaunayTriangulation::with_kernel(&RobustKernel::new(), &vertices).unwrap(); - assert!(!dt.should_run_delaunay_repair_for(topology, 0)); - assert!(!dt.should_run_delaunay_repair_for(topology, 1)); - assert!(dt.should_run_delaunay_repair_for(topology, 2)); + assert_eq!(dt.number_of_vertices(), 3); + assert_eq!(dt.number_of_simplices(), 1); } #[test] - fn test_delaunay_repair_policy_zero_insertions_never_repairs() { - assert!(!DelaunayRepairPolicy::EveryInsertion.should_repair(0)); - assert!(!DelaunayRepairPolicy::EveryN(NonZeroUsize::new(2).unwrap()).should_repair(0)); + fn test_with_kernel_insufficient_vertices_2d() { + init_tracing(); + let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0])]; + + let result: Result, (), (), 2>, _> = + DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices); + + match result.unwrap_err() { + DelaunayTriangulationConstructionError::Triangulation( + DelaunayConstructionFailure::InsufficientVertices { dimension, .. }, + ) => assert_eq!(dimension, 2), + other => panic!("Expected InsufficientVertices error, got {other:?}"), + } } #[test] - fn test_non_insertion_mutation_repair_gate_ignores_insertion_cadence() { + fn test_with_kernel_insufficient_vertices_3d() { init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), + let vertices = vec![ + vertex!([0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0]), + vertex!([0.0, 1.0, 0.0]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - let topology = dt.topology_guarantee(); - dt.set_delaunay_repair_policy(DelaunayRepairPolicy::EveryN(NonZeroUsize::new(2).unwrap())); - assert!(dt.should_run_delaunay_repair_after_mutation(topology)); + let result: Result, (), (), 3>, _> = + DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices); - dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); - assert!(!dt.should_run_delaunay_repair_after_mutation(topology)); + match result.unwrap_err() { + DelaunayTriangulationConstructionError::Triangulation( + DelaunayConstructionFailure::InsufficientVertices { dimension, .. }, + ) => assert_eq!(dimension, 3), + other => panic!("Expected InsufficientVertices error, got {other:?}"), + } } #[test] - fn test_vertex_key_valid_after_explicit_heuristic_rebuild() { + fn test_with_kernel_f32_coordinates() { init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([1.0, 1.0]), + let vertices = vec![ + vertex!([0.0f32, 0.0f32]), + vertex!([1.0f32, 0.0f32]), + vertex!([0.0f32, 1.0f32]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - // Insert a vertex normally (no heuristic rebuild during insert). - let inserted = vertex!([0.25, 0.25]); - let inserted_uuid = inserted.uuid(); - - let (outcome, _stats) = dt.insert_with_statistics(inserted).unwrap(); - let InsertionOutcome::Inserted { vertex_key, .. } = outcome else { - panic!("Expected successful insertion outcome"); - }; - - // Force a heuristic rebuild via the public repair API. - let _guard = ForceHeuristicRebuildGuard::enable(); - let outcome = dt - .repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()) - .unwrap(); - assert!( - outcome.used_heuristic(), - "Expected heuristic rebuild to be used" - ); - - // Verify the vertex is still findable by UUID after heuristic rebuild. - let remapped = dt - .tri - .tds - .vertex_key_from_uuid(&inserted_uuid) - .expect("Inserted vertex UUID missing after heuristic rebuild"); + let dt: DelaunayTriangulation, (), (), 2> = + DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices).unwrap(); - // The vertex key may have changed after heuristic rebuild, but the - // vertex should still be present and accessible. - assert!(dt.tri.tds.vertex(remapped).is_some()); - assert!(dt.validate().is_ok()); - // Original vertex_key may be stale after heuristic rebuild; that is - // expected. The important invariant is that the UUID lookup works. - let _ = vertex_key; + assert_eq!(dt.number_of_vertices(), 3); + assert_eq!(dt.number_of_simplices(), 1); } #[test] - fn test_heuristic_rebuild_preserves_global_topology() { + fn test_with_kernel_aborts_on_duplicate_uuid_in_insertion_loop() { init_tracing(); - let vertices: Vec> = vec![ + let mut vertices = vec![ vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([1.0, 1.0]), + vertex!([2.0, 0.0]), + vertex!([0.0, 2.0]), + vertex!([0.25, 0.25]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - let global_topology = GlobalTopology::Toroidal { - domain: [1.0, 1.0], - mode: ToroidalConstructionMode::PeriodicImagePoint, - }; - dt.set_global_topology(global_topology); - let _guard = ForceHeuristicRebuildGuard::enable(); - let outcome = dt - .repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()) - .unwrap(); + let dup_uuid = vertices[0].uuid(); + vertices[3].set_uuid(dup_uuid).unwrap(); - assert!( - outcome.used_heuristic(), - "Expected forced heuristic rebuild to be used" - ); - assert_eq!(dt.global_topology(), global_topology); - assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); + let result: Result, (), (), 2>, _> = + DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices); + + match result.unwrap_err() { + DelaunayTriangulationConstructionError::Triangulation( + DelaunayConstructionFailure::Tds { + reason: TdsConstructionFailure::DuplicateUuid { entity: _, uuid }, + }, + ) => assert_eq!(uuid, dup_uuid), + other => panic!("Expected DuplicateUuid error, got {other:?}"), + } } #[test] - fn test_remove_vertex_fast_path_inverse_k1() { + fn test_batch_3d_construction_with_extra_vertex_triggers_incremental_repair() { init_tracing(); - let vertices: Vec> = vec![ + 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]), + vertex!([0.3, 0.3, 0.3]), ]; - - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - dt.set_topology_guarantee(TopologyGuarantee::PLManifold); - let original_vertex_count = dt.number_of_vertices(); - let original_simplex_count = dt.number_of_simplices(); - - let simplex_key = dt.simplices().next().unwrap().0; - let inserted_vertex = vertex!([0.2, 0.2, 0.2]); - let inserted_uuid = inserted_vertex.uuid(); - dt.flip_k1_insert(simplex_key, inserted_vertex).unwrap(); - - assert_eq!(dt.number_of_vertices(), original_vertex_count + 1); - assert_eq!(dt.number_of_simplices(), original_simplex_count + 3); - - let vertex_key = dt - .vertices() - .find(|(_, v)| v.uuid() == inserted_uuid) - .map(|(k, _)| k) - .expect("Inserted vertex not found"); - - dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); - let removed_simplices = dt.remove_vertex(vertex_key).unwrap(); - - assert_eq!(removed_simplices, 4); - assert_eq!(dt.number_of_vertices(), original_vertex_count); - assert_eq!(dt.number_of_simplices(), original_simplex_count); - assert!(dt.as_triangulation().validate().is_ok()); - assert!(dt.vertices().all(|(_, v)| v.uuid() != inserted_uuid)); - } - - #[test] - fn remove_vertex_invalidates_locate_hint_and_prunes_spatial_index() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = + let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); - - let vertex_key = dt.insert(vertex!([0.25, 0.25])).unwrap(); - let hint_simplex = dt.simplices().next().map(|(key, _)| key); - dt.insertion_state.last_inserted_simplex = hint_simplex; - let mut spatial_index = HashGridIndex::::new(1.0); - for (vertex_key, vertex) in dt.vertices() { - spatial_index.insert_vertex(vertex_key, vertex.point().coords()); - } - dt.spatial_index = Some(spatial_index); - assert!(dt.insertion_state.last_inserted_simplex.is_some()); - assert!(dt.spatial_index.is_some()); - - dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); - let removed_simplices = dt.remove_vertex(vertex_key).unwrap(); - - assert!(removed_simplices > 0); - assert!(dt.insertion_state.last_inserted_simplex.is_none()); - let spatial_index = dt - .spatial_index - .as_ref() - .expect("successful vertex removal should retain the spatial index"); - let mut found_removed_key = false; - assert!( - spatial_index.for_each_candidate_vertex_key(&[0.25, 0.25], |candidate| { - found_removed_key |= candidate == vertex_key; - true - }) - ); - assert!(!found_removed_key); - assert!(dt.as_triangulation().validate().is_ok()); + assert_eq!(dt.number_of_vertices(), 5); + assert!(dt.validate().is_ok()); } #[test] - fn flip_k1_insert_invalidates_caches() { + fn test_batch_3d_construction_statistics_with_extra_vertex_triggers_incremental_repair() { init_tracing(); - let vertices: Vec> = vec![ + 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]), + vertex!([0.3, 0.3, 0.3]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - - let simplex_key = dt.simplices().next().unwrap().0; - dt.insertion_state.last_inserted_simplex = Some(simplex_key); - let mut spatial_index = HashGridIndex::::new(1.0); - for (vertex_key, vertex) in dt.vertices() { - spatial_index.insert_vertex(vertex_key, vertex.point().coords()); - } - dt.spatial_index = Some(spatial_index); - - dt.flip_k1_insert(simplex_key, vertex!([0.2, 0.2, 0.2])) - .unwrap(); - - assert!(dt.insertion_state.last_inserted_simplex.is_none()); - assert!(dt.spatial_index.is_none()); - assert!(dt.as_triangulation().validate().is_ok()); - } - - fn non_delaunay_quad_tds() -> Tds { - let mut tds: Tds = Tds::empty(); - let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); - let v1 = tds.insert_vertex_with_mapping(vertex!([4.0, 0.0])).unwrap(); - let v2 = tds.insert_vertex_with_mapping(vertex!([4.0, 2.0])).unwrap(); - let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 2.0])).unwrap(); - - tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) - .unwrap(); - tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v2, v3], None).unwrap()) - .unwrap(); - tds.construction_state = TriangulationConstructionState::Constructed; - tds.assign_neighbors().unwrap(); - tds.assign_incident_simplices().unwrap(); - tds + let (dt, stats) = + DelaunayTriangulation::<_, (), (), 3>::new_with_construction_statistics(&vertices) + .unwrap(); + assert_eq!(dt.number_of_vertices(), 5); + assert_eq!(stats.inserted, 5); + assert!(dt.validate().is_ok()); } #[test] - fn try_from_tds_rejects_non_delaunay_connectivity() { + fn test_batch_4d_forced_nonconvergent_local_repair_canonicalizes_without_stats() { init_tracing(); - let tds = non_delaunay_quad_tds(); + let vertices = vec![ + vertex!([0.0, 0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0, 0.0]), + vertex!([0.0, 1.0, 0.0, 0.0]), + vertex!([0.0, 0.0, 1.0, 0.0]), + vertex!([0.0, 0.0, 0.0, 1.0]), + vertex!([0.2, 0.2, 0.2, 0.2]), + vertex!([0.35, 0.25, 0.15, 0.3]), + ]; - let err = DelaunayTriangulation::try_from_tds(tds, AdaptiveKernel::new()) - .expect_err("checked TDS reconstruction must reject non-Delaunay connectivity"); + let _guard = ForceRepairNonconvergentGuard::enable(); + let kernel = RobustKernel::::new(); + let dt = + DelaunayTriangulation::, (), (), 4>::with_kernel(&kernel, &vertices) + .expect( + "D>=4 construction should continue after forced local repair non-convergence", + ); - assert!( - matches!( - err, - DelaunayTriangulationValidationError::VerificationFailed { .. } - ), - "expected Level 4 validation failure, got {err:?}" - ); + assert_eq!(dt.number_of_vertices(), vertices.len()); + assert!(dt.validate().is_ok()); } #[test] - fn robust_deserialize_rejects_non_delaunay_connectivity() { + fn test_batch_4d_forced_nonconvergent_local_repair_canonicalizes_with_stats() { init_tracing(); - let json = serde_json::to_string(&non_delaunay_quad_tds()).unwrap(); + let vertices = vec![ + vertex!([0.0, 0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0, 0.0]), + vertex!([0.0, 1.0, 0.0, 0.0]), + vertex!([0.0, 0.0, 1.0, 0.0]), + vertex!([0.0, 0.0, 0.0, 1.0]), + vertex!([0.2, 0.2, 0.2, 0.2]), + vertex!([0.35, 0.25, 0.15, 0.3]), + ]; - let err = - serde_json::from_str::, (), (), 2>>(&json) - .expect_err("serde reconstruction must reject non-Delaunay connectivity"); + let _guard = ForceRepairNonconvergentGuard::enable(); + let kernel = RobustKernel::::new(); + let (dt, stats) = + DelaunayTriangulation::, (), (), 4>::with_options_and_statistics( + &kernel, + &vertices, + TopologyGuarantee::DEFAULT, + ConstructionOptions::default(), + ) + .expect( + "D>=4 stats construction should continue after forced local repair non-convergence", + ); - let message = err.to_string(); - assert!( - message.contains("Delaunay verification failed"), - "serde error should preserve the Level 4 validation failure: {message}" - ); + assert_eq!(dt.number_of_vertices(), vertices.len()); + assert_eq!(stats.inserted, vertices.len()); + assert!(dt.validate().is_ok()); } - fn interior_vertex_for_k1_insert() -> Vertex { - let denominator = safe_usize_to_scalar::(D + 2) - .expect("D + 2 should convert exactly for rollback test dimensions"); - let coord = 1.0 / denominator; - vertex!([coord; D]) - } + #[test] + fn test_batch_4d_every_n_repair_cadence_runs_with_pending_seeds() { + init_tracing(); + let vertices = vec![ + vertex!([0.0, 0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0, 0.0]), + vertex!([0.0, 1.0, 0.0, 0.0]), + vertex!([0.0, 0.0, 1.0, 0.0]), + vertex!([0.0, 0.0, 0.0, 1.0]), + vertex!([0.2, 0.2, 0.2, 0.2]), + vertex!([0.35, 0.25, 0.15, 0.3]), + ]; - fn rollback_probe_vertex(point_index: usize) -> Vertex { - let dimension = - safe_usize_to_scalar::(D).expect("test dimensions should convert exactly"); - let point_index_scalar = - safe_usize_to_scalar::(point_index).expect("point index should convert exactly"); - let mut coords = [0.2 / dimension; D]; - let axis = point_index % D; - coords[axis] += point_index_scalar.mul_add(0.005, 0.02); - vertex!(coords) - } + test_hooks::reset_batch_local_repair_calls(); + let _guard = ForceRepairNonconvergentGuard::enable(); + let kernel = RobustKernel::::new(); + let options = ConstructionOptions::default() + .with_batch_repair_policy(DelaunayRepairPolicy::EveryN(NonZeroUsize::new(2).unwrap())); + let (dt, stats) = + DelaunayTriangulation::, (), (), 4>::with_options_and_statistics( + &kernel, + &vertices, + TopologyGuarantee::DEFAULT, + options, + ) + .expect("EveryN batch repair should soft-fail forced local non-convergence and finish"); - fn incident_simplex_count( - dt: &DelaunayTriangulation, (), (), D>, - vertex_key: VertexKey, - ) -> usize { - dt.simplices() - .filter(|(_, simplex)| simplex.vertices().contains(&vertex_key)) - .count() + assert_eq!(dt.number_of_vertices(), vertices.len()); + assert_eq!(stats.inserted, vertices.len()); + assert_eq!(test_hooks::batch_local_repair_calls(), 1); + assert!(dt.validate().is_ok()); } - fn assert_forced_remove_vertex_rolls_back( - dt: &mut DelaunayTriangulation, (), (), D>, - vertex_key: VertexKey, - inserted_uuid: Uuid, - ) { - let vertex_count_before = dt.number_of_vertices(); - let simplex_count_before = dt.number_of_simplices(); - let hint_simplex_before = dt.simplices().next().map(|(key, _)| key); - dt.insertion_state.last_inserted_simplex = hint_simplex_before; - let mut spatial_index = HashGridIndex::::new(1.0); - for (vertex_key, vertex) in dt.vertices() { - spatial_index.insert_vertex(vertex_key, vertex.point().coords()); - } - dt.spatial_index = Some(spatial_index); - let last_inserted_simplex_before = dt.insertion_state.last_inserted_simplex; - let spatial_index_before = dt - .spatial_index - .as_ref() - .map(HashGridIndex::::debug_snapshot); - - let _guard = ForceRepairNonconvergentGuard::enable(); - let result = dt.remove_vertex(vertex_key); - let err = result.expect_err("forced repair failure should make removal fail"); - match err { - InvariantError::Delaunay( - DelaunayTriangulationValidationError::RepairOperationFailed { - operation: DelaunayRepairOperation::VertexRemoval, - source, - }, - ) if matches!( - source.as_ref(), - DelaunayRepairError::NonConvergent { max_flips: 0, .. } - ) => - { - // Expected forced path. - } - InvariantError::Tds(TdsError::FacetSharingViolation { .. }) => { - // The insertion preflight can now reject the fan before the forced repair path. - } - other => panic!( - "expected vertex-removal RepairOperationFailed from forced repair path, got {other:?}" - ), - } - - assert_eq!(dt.number_of_vertices(), vertex_count_before); - assert_eq!(dt.number_of_simplices(), simplex_count_before); + #[test] + fn construction_options_default_uses_batch_repair_cadence() { + assert_eq!( + ConstructionOptions::default().initial_simplex_strategy(), + InitialSimplexStrategy::MaxVolume + ); + assert_eq!( + ConstructionOptions::default().batch_repair_policy(), + DelaunayRepairPolicy::EveryInsertion + ); assert_eq!( - dt.insertion_state.last_inserted_simplex, last_inserted_simplex_before, - "remove_vertex rollback should restore last_inserted_simplex" + DelaunayRepairPolicy::default(), + DelaunayRepairPolicy::EveryInsertion ); + } + + #[test] + fn construction_options_builder_roundtrip() { + let opts = ConstructionOptions::default() + .with_insertion_order(InsertionOrderStrategy::Input) + .with_dedup_policy(DedupPolicy::Exact) + .with_batch_repair_policy(DelaunayRepairPolicy::EveryN(NonZeroUsize::new(4).unwrap())) + .with_retry_policy(RetryPolicy::Disabled); + + assert_eq!(opts.insertion_order(), InsertionOrderStrategy::Input); + assert_eq!(opts.dedup_policy(), DedupPolicy::Exact); assert_eq!( - dt.spatial_index - .as_ref() - .map(HashGridIndex::::debug_snapshot), - spatial_index_before, - "remove_vertex rollback should restore spatial_index" + opts.batch_repair_policy(), + DelaunayRepairPolicy::EveryN(NonZeroUsize::new(4).unwrap()) ); - assert!(dt.vertices().any(|(_, v)| v.uuid() == inserted_uuid)); - assert!(dt.as_triangulation().validate().is_ok()); - } - - fn assert_remove_vertex_rollback() { - init_tracing(); - let vertices = simplex_vertices::(); - - let mut dt: DelaunayTriangulation, (), (), D> = - DelaunayTriangulation::new(&vertices).unwrap(); - dt.set_topology_guarantee(TopologyGuarantee::PLManifold); - - let simplex_key = dt.simplices().next().unwrap().0; - let inserted_vertex = interior_vertex_for_k1_insert::(); - let inserted_uuid = inserted_vertex.uuid(); - dt.flip_k1_insert(simplex_key, inserted_vertex).unwrap(); - - let vertex_key = dt - .vertices() - .find(|(_, v)| v.uuid() == inserted_uuid) - .map(|(k, _)| k) - .expect("Inserted vertex not found"); - - assert_forced_remove_vertex_rolls_back(&mut dt, vertex_key, inserted_uuid); + assert_eq!(opts.retry_policy(), RetryPolicy::Disabled); } - fn assert_remove_vertex_fallback_rollback() { - init_tracing(); - let vertices = simplex_vertices::(); + #[test] + fn construction_options_global_repair_fallback_toggle() { + let default_opts = ConstructionOptions::default(); + assert!(default_opts.use_global_repair_fallback); - let mut dt: DelaunayTriangulation, (), (), D> = - DelaunayTriangulation::new(&vertices).unwrap(); - dt.set_topology_guarantee(TopologyGuarantee::PLManifold); - - let mut inserted_vertices = Vec::new(); - for point_index in 0..(D + 3) { - let inserted_vertex = rollback_probe_vertex::(point_index); - let inserted_uuid = inserted_vertex.uuid(); - let vertex_key = dt - .insert(inserted_vertex) - .expect("rollback fallback fixture insertion should succeed"); - inserted_vertices.push((vertex_key, inserted_uuid)); - } + let disabled_opts = default_opts.without_global_repair_fallback(); + assert!(!disabled_opts.use_global_repair_fallback); - let (vertex_key, inserted_uuid, incident_simplices) = inserted_vertices - .iter() - .find_map(|&(vertex_key, inserted_uuid)| { - let incident_simplices = incident_simplex_count(&dt, vertex_key); - (incident_simplices != D + 1).then_some(( - vertex_key, - inserted_uuid, - incident_simplices, - )) - }) - .expect("expected at least one inserted vertex with a non-simplex star"); - assert_ne!( - incident_simplices, - D + 1, - "fallback rollback fixture must avoid the inverse-k=1 simplex-star path" + let chained_opts = ConstructionOptions::default() + .with_insertion_order(InsertionOrderStrategy::Input) + .without_global_repair_fallback() + .with_retry_policy(RetryPolicy::Disabled); + assert!(!chained_opts.use_global_repair_fallback); + assert_eq!( + chained_opts.insertion_order(), + InsertionOrderStrategy::Input ); - - assert_forced_remove_vertex_rolls_back(&mut dt, vertex_key, inserted_uuid); + assert_eq!(chained_opts.retry_policy(), RetryPolicy::Disabled); } - macro_rules! gen_remove_vertex_rollback_tests { - ($dim:literal) => { - pastey::paste! { - #[test] - fn []() { - assert_remove_vertex_rollback::<$dim>(); - } - - #[test] - fn []() { - assert_remove_vertex_fallback_rollback::<$dim>(); - } - } + #[test] + fn construction_statistics_record_insertion_tracks_inserted_common_fields() { + let mut summary = ConstructionStatistics::default(); + let stats = InsertionStatistics { + attempts: 3, + simplices_removed_during_repair: 4, + result: InsertionResult::Inserted, }; - } - gen_remove_vertex_rollback_tests!(2); - gen_remove_vertex_rollback_tests!(3); - gen_remove_vertex_rollback_tests!(4); - gen_remove_vertex_rollback_tests!(5); + summary.record_insertion(&stats); + + assert_eq!(summary.inserted, 1); + assert_eq!(summary.skipped_duplicate, 0); + assert_eq!(summary.skipped_degeneracy, 0); + assert_eq!(summary.total_attempts, 3); + assert_eq!(summary.max_attempts, 3); + assert_eq!(summary.attempts_histogram.get(3).copied().unwrap_or(0), 1); + assert_eq!(summary.used_perturbation, 1); + assert_eq!(summary.simplices_removed_total, 4); + assert_eq!(summary.simplices_removed_max, 4); + assert_eq!(stats.attempts, 3); + assert!(matches!(stats.result, InsertionResult::Inserted)); + } #[test] - fn test_repair_delaunay_with_flips_allows_pl_manifold() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([1.0, 1.0]), - ]; + fn construction_statistics_record_insertion_tracks_skipped_variants() { + let mut summary = ConstructionStatistics::default(); + let skipped_duplicate = InsertionStatistics { + attempts: 1, + simplices_removed_during_repair: 0, + result: InsertionResult::SkippedDuplicate, + }; + let skipped_degeneracy = InsertionStatistics { + attempts: 2, + simplices_removed_during_repair: 5, + result: InsertionResult::SkippedDegeneracy, + }; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - dt.set_topology_guarantee(TopologyGuarantee::PLManifold); + summary.record_insertion(&skipped_duplicate); + summary.record_insertion(&skipped_degeneracy); - let result = dt.repair_delaunay_with_flips(); - assert!( - !matches!(result, Err(DelaunayRepairError::InvalidTopology { .. })), - "Flip-based repair should be admissible under PLManifold topology" - ); + assert_eq!(summary.inserted, 0); + assert_eq!(summary.skipped_duplicate, 1); + assert_eq!(summary.skipped_degeneracy, 1); + assert_eq!(summary.total_skipped(), 2); + assert_eq!(summary.total_attempts, 3); + assert_eq!(summary.max_attempts, 2); + assert_eq!(summary.attempts_histogram.get(1).copied().unwrap_or(0), 1); + assert_eq!(summary.attempts_histogram.get(2).copied().unwrap_or(0), 1); + assert_eq!(summary.used_perturbation, 1); + assert_eq!(summary.simplices_removed_total, 5); + assert_eq!(summary.simplices_removed_max, 5); } - /// When the primary flip repair returns `NonConvergent`, the advanced repair - /// method falls back to `repair_delaunay_with_flips_robust`. On a valid - /// triangulation the robust pass succeeds, so the outcome reports no - /// heuristic rebuild. #[test] - fn test_repair_delaunay_with_flips_advanced_robust_fallback_succeeds() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([1.0, 1.0]), - ]; - let mut dt: DelaunayTriangulation, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); + fn construction_statistics_record_skip_sample_caps_at_eight_samples() { + let mut summary = ConstructionStatistics::default(); + for index in 0..10 { + let sample_index_u32 = u32::try_from(index).unwrap(); + let coordinate_base = >::from(sample_index_u32); + summary.record_skip_sample(ConstructionSkipSample { + index, + uuid: Uuid::from_u128( + >::from(sample_index_u32) + 1, + ), + coords: vec![ + coordinate_base, + coordinate_base + 0.5, + coordinate_base + 1.0, + ], + coords_available: true, + attempts: index + 1, + error: format!("skip sample #{index}"), + }); + } - let _guard = ForceRepairNonconvergentGuard::enable(); - let outcome = dt - .repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()) - .unwrap(); - assert!( - !outcome.used_heuristic(), - "Robust fallback should succeed without needing heuristic rebuild" + assert_eq!(summary.skip_samples.len(), 8); + assert_eq!(summary.skip_samples.first().map(|s| s.index), Some(0)); + assert_eq!(summary.skip_samples.last().map(|s| s.index), Some(7)); + assert_eq!( + summary.skip_samples.last().map(|s| s.uuid), + Some(Uuid::from_u128(8)) ); } - /// When the primary per-insertion repair returns `NonConvergent`, the robust - /// fallback in `maybe_repair_after_insertion` should rescue the insertion. #[test] - fn test_maybe_repair_after_insertion_robust_fallback_on_forced_nonconvergent() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let mut dt: DelaunayTriangulation, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); + fn construction_statistics_records_slowest_insertion_samples() { + let mut summary = ConstructionStatistics::default(); + for index in 0..10 { + let sample_index_u32 = u32::try_from(index).unwrap(); + summary.record_slow_insertion_sample(ConstructionSlowInsertionSample { + index, + uuid: Uuid::from_u128( + >::from(sample_index_u32) + 1, + ), + attempts: 1, + result: InsertionResult::Inserted, + elapsed_nanos: >::from(sample_index_u32) * 1_000, + simplices_after: index, + locate_calls: 1, + locate_walk_steps_total: index, + conflict_region_calls: 1, + conflict_region_simplices_total: index, + cavity_insertion_calls: 1, + global_conflict_scans: 0, + hull_extension_calls: 0, + topology_validation_calls: 1, + }); + } - let _guard = ForceRepairNonconvergentGuard::enable(); - let result = dt.insert(vertex!([0.5, 0.5])); - let inserted_key = result - .as_ref() - .copied() - .expect("Insertion should succeed via robust fallback"); - assert!( - result.is_ok(), - "Insertion should succeed via robust fallback: {result:?}" - ); - let spatial_index = dt - .spatial_index - .as_ref() - .expect("topology-only repair should preserve the duplicate-detection index"); - let mut found_inserted_key = false; + assert_eq!(summary.slow_insertions.len(), 8); + assert_eq!(summary.slow_insertions.first().map(|s| s.index), Some(9)); + assert_eq!(summary.slow_insertions.last().map(|s| s.index), Some(2)); assert!( - spatial_index.for_each_candidate_vertex_key(&[0.5, 0.5], |candidate| { - found_inserted_key |= candidate == inserted_key; - true - }) + summary + .slow_insertions + .windows(2) + .all(|pair| pair[0].elapsed_nanos >= pair[1].elapsed_nanos) ); - assert!(found_inserted_key); - assert!(dt.validate().is_ok()); } - /// Verifies that `DelaunayRepairHeuristicConfig::max_flips` caps the repair budget - /// when called through the public `repair_delaunay_with_flips_advanced` API. - /// - /// Sub-case 1: A budget of 0 on a triangulation that is already Delaunay should succeed - /// (the initial repair pass finds no violations). - /// - /// Sub-case 2: A budget of 0 on a forced-non-convergent state should hit the - /// robust fallback path (the primary pass returns `NonConvergent`, the robust - /// pass succeeds because the triangulation is actually Delaunay). #[test] - fn test_repair_advanced_max_flips_zero_on_valid_triangulation_succeeds() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([1.0, 1.0]), - ]; - let mut dt: DelaunayTriangulation, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - - // Sub-case 1: Already Delaunay — max_flips=0 should succeed (no flips needed). - let config = DelaunayRepairHeuristicConfig { - max_flips: Some(0), - ..DelaunayRepairHeuristicConfig::default() + fn log_bulk_progress_if_due_updates_progress_state_only_when_due() { + let sample = BatchProgressSample { + bulk_processed: 5, + bulk_inserted: 4, + bulk_skipped: 1, + simplex_count: 7, + perturbation_seed: 0xCAFE, }; - let outcome = dt.repair_delaunay_with_flips_advanced(config).unwrap(); - assert_eq!(outcome.stats.flips_performed, 0); - assert!( - !outcome.used_heuristic(), - "Already-Delaunay triangulation should not trigger heuristic rebuild" - ); - } - /// Sub-case 2 of the `max_flips` budget test: force the primary repair to fail - /// (via `ForceRepairNonconvergentGuard`) with `max_flips=0`, then verify the - /// robust fallback succeeds (the triangulation is actually valid). - #[test] - fn test_repair_advanced_max_flips_zero_forced_nonconvergent_hits_robust_fallback() { - init_tracing(); - let vertices: Vec> = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - vertex!([1.0, 1.0]), - ]; - let mut dt: DelaunayTriangulation, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); + let mut disabled = None; + log_bulk_progress_if_due(sample, &mut disabled); + assert!(disabled.is_none()); - let _guard = ForceRepairNonconvergentGuard::enable(); - let config = DelaunayRepairHeuristicConfig { - max_flips: Some(0), - ..DelaunayRepairHeuristicConfig::default() - }; - // The primary repair is forced to fail; the robust fallback should succeed - // because the triangulation is actually Delaunay. - let outcome = dt.repair_delaunay_with_flips_advanced(config).unwrap(); - assert_eq!( - outcome.stats.flips_performed, 0, - "max_flips=0 should prevent any flips even on the robust fallback path" - ); - assert!( - !outcome.used_heuristic(), - "Robust fallback should succeed without heuristic rebuild" - ); - } + let mut state = Some(BatchProgressState { + input_vertices: 13, + initial_simplex_vertices: 3, + bulk_vertices: 10, + progress_every: 5, + started: Instant::now(), + last_progress: Instant::now(), + last_processed: 0, + }); - /// Sub-case 3: - /// verify `max_flips=0` returns `NonConvergent`, then retry with a sufficient budget - /// and verify repair succeeds with flips performed. - #[test] - fn test_repair_advanced_max_flips_on_non_delaunay_triangulation() { - init_tracing(); + log_bulk_progress_if_due( + BatchProgressSample { + bulk_processed: 0, + ..sample + }, + &mut state, + ); + assert_eq!(state.as_ref().unwrap().last_processed, 0); - // Reuse the explicit non-Delaunay quadrilateral fixture so the primary - // 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> = - DelaunayTriangulation::from_tds_with_topology_guarantee( - tds, - kernel, - TopologyGuarantee::PLManifold, - ); - dt.set_topology_guarantee(TopologyGuarantee::PLManifold); + log_bulk_progress_if_due( + BatchProgressSample { + bulk_processed: 3, + ..sample + }, + &mut state, + ); + assert_eq!(state.as_ref().unwrap().last_processed, 0); - // max_flips=0 should fail (flips are needed but budget is zero). - let config_zero = DelaunayRepairHeuristicConfig { - max_flips: Some(0), - ..DelaunayRepairHeuristicConfig::default() - }; - // The advanced path tries primary (fails at budget=0), then robust fallback. - // The robust fallback also respects the budget, so it should also fail at 0, - // then the heuristic rebuild fires. The key assertion: it should not silently - // succeed with 0 flips on the primary path. - let outcome_zero = dt.repair_delaunay_with_flips_advanced(config_zero); - // Either heuristic rebuild succeeds or we get an error — both are acceptable. - // What would be wrong is a silent Ok with 0 flips on a non-Delaunay input. - if let Ok(ref outcome) = outcome_zero { - assert!( - outcome.used_heuristic() || outcome.stats.flips_performed > 0, - "max_flips=0 on non-Delaunay input must not silently succeed with 0 flips and no heuristic" - ); - } + log_bulk_progress_if_due(sample, &mut state); + assert_eq!(state.as_ref().unwrap().last_processed, 5); - // Now retry with a generous budget — should succeed. - let config_generous = DelaunayRepairHeuristicConfig { - max_flips: Some(100), - ..DelaunayRepairHeuristicConfig::default() - }; - // Reconstruct dt from the same raw TDS in case the previous attempt mutated it. - let tds2 = non_delaunay_quad_tds(); - let mut dt2: DelaunayTriangulation, (), (), 2> = - DelaunayTriangulation::from_tds_with_topology_guarantee( - tds2, - AdaptiveKernel::new(), - TopologyGuarantee::PLManifold, - ); - dt2.set_topology_guarantee(TopologyGuarantee::PLManifold); - let outcome_generous = dt2 - .repair_delaunay_with_flips_advanced(config_generous) - .unwrap(); - assert!( - outcome_generous.stats.flips_performed > 0, - "Generous budget should allow flips to repair the non-Delaunay triangulation" + log_bulk_progress_if_due( + BatchProgressSample { + bulk_processed: 10, + bulk_inserted: 8, + bulk_skipped: 2, + simplex_count: 11, + perturbation_seed: 0xBEEF, + }, + &mut state, ); + assert_eq!(state.as_ref().unwrap().last_processed, 10); } - /// `repair_delaunay_with_flips` delegates to `repair_delaunay_with_flips_k2_k3` - /// which requires D ≥ 2. On a 1D triangulation the inner function returns - /// `FlipError::UnsupportedDimension`, surfaced as `DelaunayRepairError::Flip`. #[test] - fn test_repair_delaunay_with_flips_returns_flip_error_for_1d() { + fn test_vertex_coords_f64_converts_f64_vertex_coords() { init_tracing(); - let vertices: Vec> = vec![vertex!([0.0]), vertex!([1.0])]; - let mut dt: DelaunayTriangulation, (), (), 1> = - DelaunayTriangulation::new(&vertices).unwrap(); + let vertex: Vertex = vertex!([1.25, -2.5, 3.75]); - let result = dt.repair_delaunay_with_flips(); - assert!( - matches!( - result, - Err(DelaunayRepairError::Flip(FlipError::UnsupportedDimension { - dimension: 1 - })) - ), - "Expected Flip(UnsupportedDimension {{ dimension: 1 }}) for D=1, got: {result:?}" - ); + assert_eq!(vertex_coords_f64(&vertex), Some(vec![1.25, -2.5, 3.75])); } - /// `repair_delaunay_with_flips_advanced` passes through non-retryable errors - /// (anything other than `NonConvergent` / `PostconditionFailed`) from the - /// inner `repair_delaunay_with_flips` call. A 1D triangulation triggers - /// `UnsupportedDimension` which must hit the `Err(err) => Err(err)` arm. #[test] - fn test_repair_delaunay_with_flips_advanced_passes_through_non_retryable_error() { + fn test_vertex_coords_f64_converts_f32_vertex_coords() { init_tracing(); - let vertices: Vec> = vec![vertex!([0.0]), vertex!([1.0])]; - let mut dt: DelaunayTriangulation, (), (), 1> = - DelaunayTriangulation::new(&vertices).unwrap(); + let vertex: Vertex = vertex!([1.25f32, -2.5f32, 3.75f32]); - let result = - dt.repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()); - assert!( - matches!( - result, - Err(DelaunayRepairError::Flip(FlipError::UnsupportedDimension { - dimension: 1 - })) - ), - "Expected non-retryable Flip(UnsupportedDimension) pass-through for D=1, got: {result:?}" - ); + assert_eq!(vertex_coords_f64(&vertex), Some(vec![1.25, -2.5, 3.75])); } - /// Macro to generate comprehensive triangulation construction tests across dimensions. - /// - /// This macro generates tests that verify all construction patterns: - /// 1. **Batch construction** - Creating a simplex with D+1 vertices + incremental insertion - /// 2. **Bootstrap from empty** - Accumulating vertices until D+1, then auto-creating simplex - /// 3. **Cavity-based continuation** - Verifying cavity algorithm works after bootstrap - /// 4. **Equivalence testing** - Bootstrap and batch produce identical structures - /// - /// # Usage - /// ```ignore - /// test_incremental_insertion!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], [0.5, 0.5]); - /// ``` - macro_rules! test_incremental_insertion { - ($dim:expr, [$($simplex_coords:expr),+ $(,)?], $interior_point:expr) => { - pastey::paste! { - // Test 1: Batch construction with incremental insertion - #[test] - fn []() { - init_tracing(); - // Build initial simplex (D+1 vertices) - let mut vertices: Vec> = vec![ - $(vertex!($simplex_coords)),+ - ]; - - // Add interior point to be inserted incrementally - vertices.push(vertex!($interior_point)); - - let expected_vertices = vertices.len(); - - let dt: DelaunayTriangulation<_, (), (), $dim> = - DelaunayTriangulation::new(&vertices).unwrap(); - - assert_eq!(dt.number_of_vertices(), expected_vertices, - "{}D: Expected {} vertices", $dim, expected_vertices); - assert!(dt.number_of_simplices() > 1, - "{}D: Expected multiple simplices, got {}", $dim, dt.number_of_simplices()); - } - - // Test 2: Bootstrap from empty triangulation - #[test] - fn []() { - init_tracing(); - // Start with empty triangulation - let mut dt: DelaunayTriangulation<_, (), (), $dim> = DelaunayTriangulation::empty(); - assert_eq!(dt.number_of_vertices(), 0); - assert_eq!(dt.number_of_simplices(), 0); - - let vertices = vec![$(vertex!($simplex_coords)),+]; - assert_eq!(vertices.len(), $dim + 1, "Test should provide exactly D+1 vertices"); - - // Insert D vertices - should accumulate without creating simplices - for (i, vertex) in vertices.iter().take($dim).enumerate() { - dt.insert(*vertex).unwrap(); - assert_eq!(dt.number_of_vertices(), i + 1, - "{}D: After inserting vertex {}, expected {} vertices", $dim, i, i + 1); - assert_eq!(dt.number_of_simplices(), 0, - "{}D: Should have 0 simplices during bootstrap (have {} vertices < D+1)", - $dim, i + 1); - } - - // Insert (D+1)th vertex - should trigger initial simplex creation - dt.insert(*vertices.last().unwrap()).unwrap(); - assert_eq!(dt.number_of_vertices(), $dim + 1); - assert_eq!(dt.number_of_simplices(), 1, - "{}D: Should have exactly 1 simplex after inserting D+1 vertices", $dim); - - // Verify triangulation is valid - assert!(dt.is_valid().is_ok(), - "{}D: Triangulation should be valid after bootstrap", $dim); - } - - // Test 3: Bootstrap continues with cavity-based insertion - #[test] - fn []() { - init_tracing(); - // Start with empty, bootstrap to initial simplex, then continue with cavity-based - let mut dt: DelaunayTriangulation<_, (), (), $dim> = DelaunayTriangulation::empty(); - - let initial_vertices = vec![$(vertex!($simplex_coords)),+]; - - // Bootstrap: insert D+1 vertices - for vertex in &initial_vertices { - dt.insert(*vertex).unwrap(); - } - assert_eq!(dt.number_of_simplices(), 1); - - // Continue with cavity-based insertion (vertex D+2 onward) - dt.insert(vertex!($interior_point)).unwrap(); - assert_eq!(dt.number_of_vertices(), $dim + 2); - assert!(dt.number_of_simplices() > 1, - "{}D: Should have multiple simplices after cavity-based insertion", $dim); - - // Verify triangulation remains valid - assert!(dt.is_valid().is_ok()); - } - - // Test 4: Bootstrap equivalent to batch construction - #[test] - fn []() { - init_tracing(); - // Compare bootstrap path vs batch construction - let vertices = vec![$(vertex!($simplex_coords)),+]; - - // Path A: Bootstrap from empty - let mut dt_bootstrap: DelaunayTriangulation<_, (), (), $dim> = - DelaunayTriangulation::empty(); - for vertex in &vertices { - dt_bootstrap.insert(*vertex).unwrap(); - } - - // Path B: Batch construction - let dt_batch: DelaunayTriangulation<_, (), (), $dim> = - DelaunayTriangulation::new(&vertices).unwrap(); - - // Both should produce identical structure - assert_eq!(dt_bootstrap.number_of_vertices(), dt_batch.number_of_vertices(), - "{}D: Bootstrap and batch should have same vertex count", $dim); - assert_eq!(dt_bootstrap.number_of_simplices(), dt_batch.number_of_simplices(), - "{}D: Bootstrap and batch should have same simplex count", $dim); + #[test] + fn test_vertex_coords_f64_rejects_non_finite_coords() { + init_tracing(); + let nan_vertex: Vertex = VertexBuilder::default() + .point(Point::new([1.0, f64::NAN, 3.0])) + .build() + .unwrap(); + let infinite_vertex: Vertex = VertexBuilder::default() + .point(Point::new([1.0, f64::INFINITY, 3.0])) + .build() + .unwrap(); - // Both should be valid - assert!(dt_bootstrap.is_valid().is_ok()); - assert!(dt_batch.is_valid().is_ok()); - } - } - }; + assert_eq!(vertex_coords_f64(&nan_vertex), None); + assert_eq!(vertex_coords_f64(&infinite_vertex), None); } - // 2D: Triangle + interior point - test_incremental_insertion!(2, [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]], [0.5, 0.5]); - - // 3D: Tetrahedron + interior point - test_incremental_insertion!( - 3, - [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0] - ], - [0.2, 0.2, 0.2] - ); + fn coord_sequence_2d(vertices: &[Vertex]) -> Vec<[f64; 2]> { + vertices.iter().map(|v| *v.point().coords()).collect() + } - // 4D: 4-simplex + interior point - test_incremental_insertion!( - 4, - [ - [0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 1.0] - ], - [0.2, 0.2, 0.2, 0.2] - ); + #[test] + fn order_vertices_input_preserves_order() { + init_tracing(); + let vertices = vec![ + vertex!([2.0, 0.0]), + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + ]; + let expected = coord_sequence_2d(&vertices); - // 5D: 5-simplex + interior point - test_incremental_insertion!( - 5, - [ - [0.0, 0.0, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 1.0] - ], - [0.2, 0.2, 0.2, 0.2, 0.2] - ); + let ordered = order_vertices_by_strategy(vertices, InsertionOrderStrategy::Input); - // ========================================================================= - // empty() / with_empty_kernel() tests - // ========================================================================= + assert_eq!(coord_sequence_2d(&ordered), expected); + } #[test] - fn test_empty_creates_empty_triangulation() { + fn dedup_exact_sorted_without_grid() { init_tracing(); - let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); + let vertices = vec![ + vertex!([1.0, 0.0]), + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; - assert_eq!(dt.number_of_vertices(), 0); - assert_eq!(dt.number_of_simplices(), 0); - // dim() returns -1 for empty triangulation - assert_eq!(dt.dim(), -1); + let unique = dedup_vertices_exact_sorted(vertices); + + assert_eq!( + coord_sequence_2d(&unique), + vec![[0.0, 0.0], [0.0, 1.0], [1.0, 0.0]] + ); } #[test] - fn test_empty_supports_incremental_insertion() { + fn dedup_exact_grid_fallback() { init_tracing(); - // Verify empty triangulation supports incremental insertion via bootstrap - let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); - assert_eq!(dt.number_of_vertices(), 0); + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 0.0]), + ]; + let mut grid = HashGridIndex::::new(1.0e-10); - // Can now insert into empty triangulation - bootstrap phase - dt.insert(vertex!([0.0, 0.0])).unwrap(); - dt.insert(vertex!([1.0, 0.0])).unwrap(); - assert_eq!(dt.number_of_simplices(), 0); // Still in bootstrap + let unique = dedup_vertices_exact_hash_grid(vertices, &mut grid); - dt.insert(vertex!([0.0, 1.0])).unwrap(); - assert_eq!(dt.number_of_simplices(), 1); // Initial simplex created + assert_eq!(coord_sequence_2d(&unique), vec![[0.0, 0.0], [1.0, 0.0]]); + + let vertices_6d = vec![ + vertex!([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + ]; + let mut unusable_grid = HashGridIndex::::new(1.0e-10); + + let fallback_unique = dedup_vertices_exact_hash_grid(vertices_6d, &mut unusable_grid); + + assert_eq!(fallback_unique.len(), 1); } #[test] - fn test_validation_policy_defaults_to_on_suspicion() { + fn epsilon_dedup_quantized_paths() { init_tracing(); - // empty() -> Triangulation::new_empty() -> ValidationPolicy::default() - let dt_empty: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); - assert_eq!(dt_empty.validation_policy(), ValidationPolicy::OnSuspicion); - let vertices = vec![ vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), + vertex!([0.09, 0.0]), + vertex!([0.25, 0.0]), ]; - // new() -> with_kernel() -> explicit validation_policy initialization - let dt_new: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - assert_eq!(dt_new.validation_policy(), ValidationPolicy::OnSuspicion); + let unique = dedup_vertices_epsilon_quantized(vertices, 0.1); - // with_kernel() constructor path should also use the default policy - let dt_with_kernel: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices).unwrap(); - assert_eq!( - dt_with_kernel.validation_policy(), - ValidationPolicy::OnSuspicion - ); + assert_eq!(coord_sequence_2d(&unique), vec![[0.0, 0.0], [0.25, 0.0]]); - // try_from_tds() is a separate reconstruction path and should also - // default to OnSuspicion after validation succeeds. - let tds = - Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); - let dt_from_tds: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::try_from_tds(tds, FastKernel::new()).unwrap(); - assert_eq!( - dt_from_tds.validation_policy(), - ValidationPolicy::OnSuspicion - ); + let zero_epsilon_vertices = vec![vertex!([0.0, 0.0]), vertex!([0.0, 0.0])]; + let zero_epsilon_unique = dedup_vertices_epsilon_quantized(zero_epsilon_vertices, 0.0); + assert_eq!(zero_epsilon_unique.len(), 2); + + let nonfinite_vertices = vec![ + vertex!([0.0, 0.0]), + Vertex::new_with_uuid(Point::new([f64::NAN, 0.0]), Uuid::new_v4(), None), + ]; + let nonfinite_unique = dedup_vertices_epsilon_quantized(nonfinite_vertices, 0.1); + assert_eq!(nonfinite_unique.len(), 2); + + let vertices_6d = vec![ + vertex!([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + vertex!([0.01, 0.0, 0.0, 0.0, 0.0, 0.0]), + ]; + let fallback_unique = dedup_vertices_epsilon_quantized(vertices_6d, 0.1); + assert_eq!(fallback_unique.len(), 1); } #[test] - fn test_validation_policy_setter_and_getter_roundtrip() { + fn dedup_epsilon_grid_fallback() { init_tracing(); let vertices = vec![ vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), + vertex!([0.05, 0.0]), + vertex!([0.25, 0.0]), ]; + let mut grid = HashGridIndex::::new(0.1); - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); + let unique = dedup_vertices_epsilon_hash_grid(vertices, 0.1, &mut grid); + + assert_eq!(coord_sequence_2d(&unique), vec![[0.0, 0.0], [0.25, 0.0]]); - // Getter reflects the underlying Triangulation policy. - assert_eq!(dt.validation_policy(), ValidationPolicy::OnSuspicion); - assert_eq!(dt.tri.validation_policy, ValidationPolicy::OnSuspicion); + let fallback_vertices = vec![vertex!([0.0, 0.0]), vertex!([0.05, 0.0])]; + let mut unusable_grid = HashGridIndex::::new(0.0); + + let fallback_unique = + dedup_vertices_epsilon_hash_grid(fallback_vertices, 0.1, &mut unusable_grid); - dt.set_validation_policy(ValidationPolicy::Always); - assert_eq!(dt.validation_policy(), ValidationPolicy::Always); - assert_eq!(dt.tri.validation_policy, ValidationPolicy::Always); + assert_eq!(fallback_unique.len(), 1); + } - dt.set_validation_policy(ValidationPolicy::Never); - assert_eq!(dt.validation_policy(), ValidationPolicy::Never); - assert_eq!(dt.tri.validation_policy, ValidationPolicy::Never); + #[test] + fn preprocess_falls_back_when_grid_unusable() { + init_tracing(); + let exact_vertices = vec![ + vertex!([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + vertex!([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + ]; - dt.set_validation_policy(ValidationPolicy::OnSuspicion); - assert_eq!(dt.validation_policy(), ValidationPolicy::OnSuspicion); - assert_eq!(dt.tri.validation_policy, ValidationPolicy::OnSuspicion); - } + let exact = TestDelaunay::<6>::preprocess_vertices_for_construction( + &exact_vertices, + DedupPolicy::Exact, + InsertionOrderStrategy::Input, + InitialSimplexStrategy::First, + ) + .unwrap(); - // ========================================================================= - // with_kernel() tests - // ========================================================================= + assert_eq!(exact.primary_slice(&exact_vertices).len(), 2); + assert!(exact.grid_cell_size().is_none()); - #[test] - fn test_with_kernel_fast_kernel() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), + let epsilon_vertices = vec![ + vertex!([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]), + vertex!([0.01, 0.0, 0.0, 0.0, 0.0, 0.0]), + vertex!([0.5, 0.0, 0.0, 0.0, 0.0, 0.0]), ]; - let dt: DelaunayTriangulation, (), (), 2> = - DelaunayTriangulation::with_kernel(&FastKernel::new(), &vertices).unwrap(); + let epsilon = TestDelaunay::<6>::preprocess_vertices_for_construction( + &epsilon_vertices, + DedupPolicy::Epsilon { tolerance: 0.1 }, + InsertionOrderStrategy::Input, + InitialSimplexStrategy::First, + ) + .unwrap(); - assert_eq!(dt.number_of_vertices(), 3); - assert_eq!(dt.number_of_simplices(), 1); + assert_eq!(epsilon.primary_slice(&epsilon_vertices).len(), 2); + assert!(epsilon.grid_cell_size().is_none()); } #[test] - fn test_with_kernel_robust_kernel() { + fn preprocess_zero_epsilon_keeps_base() { init_tracing(); let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), + vertex!([0.0, 0.0, 0.0]), + vertex!([0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0]), ]; - let dt: DelaunayTriangulation, (), (), 2> = - DelaunayTriangulation::with_kernel(&RobustKernel::new(), &vertices).unwrap(); + let preprocess = TestDelaunay::<3>::preprocess_vertices_for_construction( + &vertices, + DedupPolicy::Epsilon { tolerance: 0.0 }, + InsertionOrderStrategy::Input, + InitialSimplexStrategy::Balanced, + ) + .unwrap(); - assert_eq!(dt.number_of_vertices(), 3); - assert_eq!(dt.number_of_simplices(), 1); + assert!(preprocess.grid_cell_size().is_some()); + assert_eq!(preprocess.primary_slice(&vertices).len(), vertices.len()); + assert!(preprocess.fallback_slice().is_none()); } #[test] - fn test_with_kernel_insufficient_vertices_2d() { + fn quantize_and_neighbor_edges() { init_tracing(); - let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0])]; + assert_eq!(quantize_coords(&[0.25, -0.25], 10.0), Some([2, -3])); + assert_eq!(quantize_coords(&[f64::NAN, 0.0], 10.0), None); + assert_eq!(quantize_coords(&[1.0e308, 0.0], 1.0e308), None); - let result: Result, (), (), 2>, _> = - DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices); + let mut visited = Vec::new(); + let mut current = [0_i64, 0_i64]; + let completed = visit_quantized_neighbors(0, &[4, 7], &mut current, &mut |neighbor| { + visited.push(neighbor); + visited.len() < 4 + }); - assert!(result.is_err()); - match result { - Err(DelaunayTriangulationConstructionError::Triangulation( - DelaunayConstructionFailure::InsufficientVertices { dimension, .. }, - )) => { - assert_eq!(dimension, 2); - } - _ => panic!("Expected InsufficientVertices error"), - } + assert!(!completed); + assert_eq!(visited.len(), 4); } #[test] - fn test_with_kernel_insufficient_vertices_3d() { + fn hilbert_fallback_for_nonfinite_coords() { init_tracing(); 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!([1.0, 0.0]), + Vertex::new_with_uuid(Point::new([f64::NAN, 0.0]), Uuid::new_v4(), None), + vertex!([0.0, 0.0]), ]; - let result: Result, (), (), 3>, _> = - DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices); + let ordered = order_vertices_hilbert(vertices, true); - assert!(result.is_err()); - match result { - Err(DelaunayTriangulationConstructionError::Triangulation( - DelaunayConstructionFailure::InsufficientVertices { dimension, .. }, - )) => { - assert_eq!(dimension, 3); - } - _ => panic!("Expected InsufficientVertices error"), - } + assert_eq!(ordered.len(), 3); + assert!( + ordered.iter().any(|v| v.point().coords()[0].is_nan()), + "fallback ordering should preserve the non-finite vertex" + ); + assert!( + ordered + .iter() + .any(|v| coords_equal_exact(v.point().coords(), &[0.0, 0.0])) + ); + assert!( + ordered + .iter() + .any(|v| coords_equal_exact(v.point().coords(), &[1.0, 0.0])) + ); } #[test] - fn test_with_kernel_f32_coordinates() { + fn hilbert_fallback_for_unsupported_dim() { init_tracing(); - let vertices = vec![ - vertex!([0.0f32, 0.0f32]), - vertex!([1.0f32, 0.0f32]), - vertex!([0.0f32, 1.0f32]), - ]; + let vertices = vec![vertex!([1.0; 17]), vertex!([0.0; 17])]; - let dt: DelaunayTriangulation, (), (), 2> = - DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices).unwrap(); + let ordered = order_vertices_hilbert(vertices, true); - assert_eq!(dt.number_of_vertices(), 3); - assert_eq!(dt.number_of_simplices(), 1); + assert!(coords_equal_exact(ordered[0].point().coords(), &[0.0; 17])); + assert!(coords_equal_exact(ordered[1].point().coords(), &[1.0; 17])); } - // ========================================================================= - // Query method tests - // ========================================================================= - #[test] - fn test_number_of_vertices_minimal_simplex() { + fn test_select_balanced_simplex_indices_insufficient_vertices() { init_tracing(); - let vertices = vec![ + let vertices: Vec> = 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::new(&vertices).unwrap(); - - assert_eq!(dt.number_of_vertices(), 4); + let result = select_balanced_simplex_indices(&vertices); + assert!(result.is_none()); } #[test] - fn test_number_of_simplices_minimal_simplex() { + fn test_select_balanced_simplex_indices_rejects_non_finite_coords() { init_tracing(); - let vertices = vec![ + let vertices: Vec> = 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]), + Vertex::new_with_uuid(Point::new([f64::NAN, 0.0, 0.0]), Uuid::new_v4(), None), ]; - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - - // Minimal 3D simplex has exactly 1 tetrahedron - assert_eq!(dt.number_of_simplices(), 1); + let result = select_balanced_simplex_indices(&vertices); + assert!(result.is_none()); } - #[test] - fn test_number_of_simplices_after_insertion() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); + macro_rules! max_volume_axis_simplex_test { + ($test_name:ident, $dimension:literal, [$($coords:expr),+ $(,)?], [$($expected_idx:expr),+ $(,)?]) => { + #[test] + fn $test_name() { + init_tracing(); + let vertices: Vec> = vec![$(vertex!($coords)),+]; - assert_eq!(dt.number_of_simplices(), 1); + let result = select_max_volume_simplex_indices(&vertices) + .expect("max-volume simplex selection failed"); + let expected_indices = [$($expected_idx),+]; - // Insert interior point - should create 3 triangles - dt.insert(vertex!([0.3, 0.3])).unwrap(); - assert_eq!(dt.number_of_simplices(), 3); + assert_eq!(result.len(), expected_indices.len()); + for expected_idx in expected_indices { + assert!( + result.contains(&expected_idx), + "expected selected simplex {result:?} to contain vertex index {expected_idx}" + ); + } + } + }; } - #[test] - fn test_dim_returns_correct_dimension() { - init_tracing(); - let vertices_2d = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let dt_2d: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices_2d).unwrap(); - assert_eq!(dt_2d.dim(), 2); + max_volume_axis_simplex_test!( + test_select_max_volume_simplex_indices_prefers_largest_triangle_2d, + 2, + [ + [0.0, 0.0], + [1.0, 0.0], + [0.0, 1.0], + [10.0, 0.0], + [0.0, 10.0], + [1.0, 1.0], + ], + [0, 3, 4] + ); - let vertices_3d = 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_3d: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices_3d).unwrap(); - assert_eq!(dt_3d.dim(), 3); + max_volume_axis_simplex_test!( + test_select_max_volume_simplex_indices_prefers_largest_tetrahedron, + 3, + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [10.0, 0.0, 0.0], + [0.0, 10.0, 0.0], + [0.0, 0.0, 10.0], + ], + [0, 4, 5, 6] + ); - let vertices_4d = vec![ - vertex!([0.0, 0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0, 0.0]), - vertex!([0.0, 1.0, 0.0, 0.0]), - vertex!([0.0, 0.0, 1.0, 0.0]), - vertex!([0.0, 0.0, 0.0, 1.0]), - ]; - let dt_4d: DelaunayTriangulation<_, (), (), 4> = - DelaunayTriangulation::new(&vertices_4d).unwrap(); - assert_eq!(dt_4d.dim(), 4); - } + max_volume_axis_simplex_test!( + test_select_max_volume_simplex_indices_prefers_largest_simplex_4d, + 4, + [ + [0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + [10.0, 0.0, 0.0, 0.0], + [0.0, 10.0, 0.0, 0.0], + [0.0, 0.0, 10.0, 0.0], + [0.0, 0.0, 0.0, 10.0], + ], + [0, 5, 6, 7, 8] + ); - // ========================================================================= - // insert() tests - // ========================================================================= + max_volume_axis_simplex_test!( + test_select_max_volume_simplex_indices_prefers_largest_simplex_5d, + 5, + [ + [0.0, 0.0, 0.0, 0.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0], + [10.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 10.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 10.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 10.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 10.0], + ], + [0, 6, 7, 8, 9, 10] + ); #[test] - fn test_insert_single_interior_point_2d() { + fn test_select_max_volume_simplex_indices_rejects_degenerate_pool() { init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), + let vertices: Vec> = vec![ + vertex!([0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0]), + vertex!([2.0, 0.0, 0.0]), + vertex!([3.0, 0.0, 0.0]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - - assert_eq!(dt.number_of_vertices(), 3); - assert_eq!(dt.number_of_simplices(), 1); - - let v_key = dt.insert(vertex!([0.3, 0.3])).unwrap(); - - // Verify insertion succeeded - assert_eq!(dt.number_of_vertices(), 4); - assert_eq!(dt.number_of_simplices(), 3); - - // Verify the returned key can access the vertex - assert!(dt.tri.tds.vertex(v_key).is_some()); + let result = select_max_volume_simplex_indices(&vertices); + assert!(result.is_none()); } #[test] - fn test_insert_multiple_sequential_points_2d() { + fn test_reorder_vertices_for_simplex_valid_and_invalid() { init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), + let vertices: Vec> = 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]), + vertex!([2.0, 2.0, 2.0]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - - // Insert 3 interior points sequentially - dt.insert(vertex!([0.3, 0.3])).unwrap(); - assert_eq!(dt.number_of_vertices(), 4); + let indices = [2_usize, 0, 3, 1]; + let reordered = + reorder_vertices_for_simplex(&vertices, &indices).expect("expected valid reorder"); - dt.insert(vertex!([0.5, 0.2])).unwrap(); - assert_eq!(dt.number_of_vertices(), 5); + let expected_first: Vec<[f64; 3]> = + indices.iter().map(|&i| (&vertices[i]).into()).collect(); + let actual_first: Vec<[f64; 3]> = reordered.iter().take(4).map(Into::into).collect(); + assert_eq!(actual_first, expected_first); - dt.insert(vertex!([0.2, 0.5])).unwrap(); - assert_eq!(dt.number_of_vertices(), 6); + let remaining_expected: Vec<[f64; 3]> = vertices + .iter() + .enumerate() + .filter(|(idx, _)| !indices.contains(idx)) + .map(|(_, v)| (*v).into()) + .collect(); + let remaining_actual: Vec<[f64; 3]> = reordered.iter().skip(4).map(Into::into).collect(); + assert_eq!(remaining_actual, remaining_expected); - // All vertices should be present - assert!(dt.number_of_simplices() > 1); + assert!(reorder_vertices_for_simplex(&vertices, &[0, 1, 2]).is_none()); + assert!(reorder_vertices_for_simplex(&vertices, &[0, 1, 1, 2]).is_none()); + assert!(reorder_vertices_for_simplex(&vertices, &[0, 1, 2, 99]).is_none()); } #[test] - fn test_insert_multiple_sequential_points_3d() { + fn test_preprocess_vertices_for_construction_balanced_sets_fallback() { init_tracing(); - let vertices = vec![ + let vertices: Vec> = 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]), + vertex!([2.0, 2.0, 2.0]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - - // Insert 3 interior points sequentially (well inside the tetrahedron) - dt.insert(vertex!([0.1, 0.1, 0.1])).unwrap(); - assert_eq!(dt.number_of_vertices(), 5); - - dt.insert(vertex!([0.15, 0.15, 0.1])).unwrap(); - assert_eq!(dt.number_of_vertices(), 6); - - dt.insert(vertex!([0.1, 0.15, 0.15])).unwrap(); - assert_eq!(dt.number_of_vertices(), 7); + let preprocess = DelaunayTriangulation::, (), (), 3>::preprocess_vertices_for_construction( + &vertices, + DedupPolicy::Off, + InsertionOrderStrategy::Input, + InitialSimplexStrategy::Balanced, + ) + .expect("preprocess failed"); - assert!(dt.number_of_simplices() > 1); + assert!(preprocess.fallback_slice().is_some()); + assert_eq!(preprocess.primary_slice(&vertices).len(), vertices.len()); + assert_eq!(preprocess.fallback_slice().unwrap().len(), vertices.len()); + assert!(preprocess.grid_cell_size().is_some()); } #[test] - fn test_insert_updates_last_inserted_simplex() { + fn test_preprocess_vertices_for_construction_max_volume_sets_largest_simplex_first() { init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), + let vertices: Vec> = 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]), + vertex!([10.0, 0.0, 0.0]), + vertex!([0.0, 10.0, 0.0]), + vertex!([0.0, 0.0, 10.0]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); + let preprocess = DelaunayTriangulation::< + AdaptiveKernel, + (), + (), + 3, + >::preprocess_vertices_for_construction( + &vertices, + DedupPolicy::Off, + InsertionOrderStrategy::Input, + InitialSimplexStrategy::MaxVolume, + ) + .expect("preprocess failed"); - // Initially no last_inserted_simplex - assert!(dt.insertion_state.last_inserted_simplex.is_none()); + let primary = preprocess.primary_slice(&vertices); + assert!(primary.len() >= 4); + let first_simplex = &primary[..4]; + let first_simplex_contains = |expected_coords: [f64; 3]| { + first_simplex.iter().any(|vertex| { + vertex + .point() + .coords() + .iter() + .zip(expected_coords) + .all(|(actual, expected)| (*actual - expected).abs() <= f64::EPSILON) + }) + }; - // After insertion, should have a cached simplex - dt.insert(vertex!([0.3, 0.3])).unwrap(); - assert!(dt.insertion_state.last_inserted_simplex.is_some()); + assert!(preprocess.fallback_slice().is_some()); + assert!(first_simplex_contains([0.0, 0.0, 0.0])); + assert!(first_simplex_contains([10.0, 0.0, 0.0])); + assert!(first_simplex_contains([0.0, 10.0, 0.0])); + assert!(first_simplex_contains([0.0, 0.0, 10.0])); } #[test] - fn test_new_with_exact_minimum_vertices() { + fn test_preprocess_vertices_rejects_invalid_epsilon_tolerance() { init_tracing(); - // 2D: exactly 3 vertices (minimum for 2D simplex) - let vertices_2d = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), - ]; - let dt_2d: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices_2d).unwrap(); - assert_eq!(dt_2d.number_of_vertices(), 3); - assert_eq!(dt_2d.number_of_simplices(), 1); - - // 3D: exactly 4 vertices (minimum for 3D simplex) - let vertices_3d = vec![ + let vertices: Vec> = 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_3d: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices_3d).unwrap(); - assert_eq!(dt_3d.number_of_vertices(), 4); - assert_eq!(dt_3d.number_of_simplices(), 1); + + let result = DelaunayTriangulation::, (), (), 3>::preprocess_vertices_for_construction( + &vertices, + DedupPolicy::Epsilon { tolerance: -1.0 }, + InsertionOrderStrategy::Input, + InitialSimplexStrategy::First, + ); + + assert!(matches!( + result, + Err(DelaunayTriangulationConstructionError::Triangulation( + DelaunayConstructionFailure::GeometricDegeneracy { .. } + )) + )); } #[test] - fn test_tds_accessor_provides_readonly_access() { + fn stats_preprocess_error_defaults() { init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), + let vertices: Vec> = 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<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); + let options = ConstructionOptions::default().with_dedup_policy(DedupPolicy::Epsilon { + tolerance: f64::NAN, + }); + + let error = + DelaunayTriangulation::, (), (), 3>::with_options_and_statistics( + &AdaptiveKernel::new(), + &vertices, + TopologyGuarantee::PLManifold, + options, + ) + .expect_err("NaN epsilon should fail during preprocessing"); - // Access TDS via immutable reference - let tds = dt.tds(); - assert_eq!(tds.number_of_vertices(), 3); - assert_eq!(tds.number_of_simplices(), 1); + assert_eq!(error.statistics.inserted, 0); + assert_eq!(error.statistics.total_skipped(), 0); + assert_eq!(error.statistics.total_attempts, 0); + assert!(error.statistics.skip_samples.is_empty()); + assert!(matches!( + error.error, + DelaunayTriangulationConstructionError::Triangulation(_) + )); + } - // Verify we can call other TDS methods - assert!(tds.is_valid().is_ok()); - assert!(tds.simplex_keys().next().is_some()); + fn vertices_from_coords_permutation_3d( + coords: &[[f64; 3]], + permutation: &[usize], + ) -> Vec> { + permutation.iter().map(|&i| vertex!(coords[i])).collect() } #[test] - fn test_internal_tds_access() { + fn test_bulk_construction_skips_near_duplicate_coordinates_3d() { init_tracing(); - let vertices = vec![ + // Test that epsilon-based deduplication removes near-duplicates + let vertices: Vec> = 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]), + vertex!([0.25, 0.25, 0.25]), + // Near-duplicate within tolerance 1e-10 + vertex!([0.25 + 5e-11, 0.25, 0.25]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - assert_eq!(dt.number_of_vertices(), 4); + let opts = ConstructionOptions::default() + .with_dedup_policy(DedupPolicy::Epsilon { tolerance: 1e-10 }) + .with_retry_policy(RetryPolicy::Disabled); + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new_with_options(&vertices, opts).unwrap(); - // Internal code can access TDS directly for mutations - let tds = &mut dt.tri.tds; - assert_eq!(tds.number_of_vertices(), 4); - assert_eq!(tds.number_of_simplices(), 1); + assert_eq!(dt.number_of_vertices(), 5); + assert!(dt.validate().is_ok()); + } - // Can call mutating methods like remove_duplicate_simplices - let result = tds.remove_duplicate_simplices(); - assert!(result.is_ok()); + fn coord_sequence_3d(vertices: &[Vertex]) -> Vec<[f64; 3]> { + vertices.iter().map(Into::into).collect() } #[test] - fn test_tds_accessor_reflects_insertions() { + fn test_insertion_order_hilbert_is_deterministic_across_permutations_3d() { init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([1.0, 0.0]), - vertex!([0.0, 1.0]), + let coords: [[f64; 3]; 8] = [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [1.0, 1.0, 1.0], + [2.0, 0.0, 1.0], + [-1.0, 5.0, 0.0], + [3.0, 2.0, 1.0], ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); - // Before insertion - assert_eq!(dt.tds().number_of_vertices(), 3); + let permutations: [&[usize]; 4] = [ + &[0, 1, 2, 3, 4, 5, 6, 7], + &[7, 6, 5, 4, 3, 2, 1, 0], + &[2, 3, 4, 5, 6, 7, 0, 1], + &[1, 3, 5, 7, 0, 2, 4, 6], + ]; + + // Test both dedup_quantized=false (sort-only) and dedup_quantized=true + // (the real code path used by order_vertices_by_strategy). + let expected_no_dedup = vertices_from_coords_permutation_3d(&coords, permutations[0]); + let expected_no_dedup = + coord_sequence_3d(&order_vertices_hilbert(expected_no_dedup, false)); + + let expected_dedup = vertices_from_coords_permutation_3d(&coords, permutations[0]); + let expected_dedup = coord_sequence_3d(&order_vertices_hilbert(expected_dedup, true)); - // Insert a new vertex - dt.insert(vertex!([0.3, 0.3])).unwrap(); + for perm in &permutations[1..] { + let vertices = vertices_from_coords_permutation_3d(&coords, perm); + let got = coord_sequence_3d(&order_vertices_hilbert(vertices, false)); + assert_eq!(got, expected_no_dedup); - // After insertion, TDS accessor reflects the change - assert_eq!(dt.tds().number_of_vertices(), 4); - assert!(dt.tds().number_of_simplices() > 1); + let vertices = vertices_from_coords_permutation_3d(&coords, perm); + let got = coord_sequence_3d(&order_vertices_hilbert(vertices, true)); + assert_eq!(got, expected_dedup); + } } - #[test] - fn test_tds_accessors_maintain_validation_invariants() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0, 0.0]), - vertex!([0.0, 1.0, 0.0, 0.0]), - vertex!([0.0, 0.0, 1.0, 0.0]), - vertex!([0.0, 0.0, 0.0, 1.0]), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 4> = - DelaunayTriangulation::new(&vertices).unwrap(); - - // Verify TDS is valid through accessor - assert!(dt.tds().is_valid().is_ok()); + // ========================================================================= + // HILBERT DEDUP — GENERIC HELPERS + // ========================================================================= - // Insert additional vertex - dt.insert(vertex!([0.2, 0.2, 0.2, 0.2])).unwrap(); + /// Build D+1 standard simplex vertices: origin + D unit vectors. + fn simplex_vertices() -> Vec> { + let mut verts = Vec::with_capacity(D + 1); + verts.push(vertex!([0.0; D])); + for i in 0..D { + let mut coords = [0.0; D]; + coords[i] = 1.0; + verts.push(vertex!(coords)); + } + verts + } - // TDS should still be valid after mutation - assert!(dt.tds().is_valid().is_ok()); - assert!(dt.tds().validate().is_ok()); + /// Build simplex vertices plus exact duplicates of the first two. + fn simplex_with_duplicates() -> (Vec>, usize) { + let mut verts = simplex_vertices::(); + let distinct = verts.len(); + // Duplicate the origin and first unit vector + verts.push(vertex!([0.0; D])); + let mut unit = [0.0; D]; + unit[0] = 1.0; + verts.push(vertex!(unit)); + (verts, distinct) } - #[test] - fn test_bootstrap_with_custom_kernel() { - init_tracing(); - // Verify bootstrap works with RobustKernel - let mut dt: DelaunayTriangulation, (), (), 3> = - DelaunayTriangulation::with_empty_kernel(RobustKernel::new()); + /// Build simplex vertices plus an interior point (all distinct). + fn simplex_with_interior() -> Vec> { + let mut verts = simplex_vertices::(); + let dimension = safe_usize_to_scalar::(D).expect("test dimensions fit in f64"); + let interior = [0.1_f64 / dimension; D]; + verts.push(vertex!(interior)); + verts + } - assert_eq!(dt.number_of_vertices(), 0); + // ========================================================================= + // HILBERT DEDUP — MACRO-GENERATED PER-DIMENSION TESTS (2D–5D) + // ========================================================================= - // Bootstrap with robust predicates - dt.insert(vertex!([0.0, 0.0, 0.0])).unwrap(); - dt.insert(vertex!([1.0, 0.0, 0.0])).unwrap(); - dt.insert(vertex!([0.0, 1.0, 0.0])).unwrap(); - assert_eq!(dt.number_of_simplices(), 0); // Still bootstrapping + /// Generate Hilbert-sort dedup tests for a given dimension: + /// + /// - exact duplicates are removed + /// - distinct points are preserved + /// - all-identical inputs collapse to 1 + macro_rules! gen_hilbert_dedup_tests { + ($dim:literal) => { + pastey::paste! { + #[test] + fn []() { + init_tracing(); + let (vertices, distinct) = simplex_with_duplicates::<$dim>(); + assert!(vertices.len() > distinct); + let result = order_vertices_hilbert(vertices, true); + assert_eq!( + result.len(), + distinct, + "{}D: exact duplicates should be removed", + $dim + ); + } - dt.insert(vertex!([0.0, 0.0, 1.0])).unwrap(); - assert_eq!(dt.number_of_simplices(), 1); // Initial simplex created + #[test] + fn []() { + init_tracing(); + let vertices = simplex_with_interior::<$dim>(); + let expected = vertices.len(); + let result = order_vertices_hilbert(vertices, true); + assert_eq!( + result.len(), + expected, + "{}D: distinct points should all be preserved", + $dim + ); + } - assert!(dt.is_valid().is_ok()); + #[test] + fn []() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.5; $dim]), + vertex!([0.5; $dim]), + vertex!([0.5; $dim]), + ]; + let result = order_vertices_hilbert(vertices, true); + assert_eq!( + result.len(), + 1, + "{}D: all-identical inputs should collapse to 1", + $dim + ); + } + } + }; } + gen_hilbert_dedup_tests!(2); + gen_hilbert_dedup_tests!(3); + gen_hilbert_dedup_tests!(4); + gen_hilbert_dedup_tests!(5); + // ========================================================================= - // Coverage-oriented tests + // HILBERT DEDUP — STANDALONE EDGE-CASE TESTS // ========================================================================= #[test] - fn test_with_kernel_aborts_on_duplicate_uuid_in_insertion_loop() { - init_tracing(); - let mut vertices = vec![ - vertex!([0.0, 0.0]), - vertex!([2.0, 0.0]), - vertex!([0.0, 2.0]), - vertex!([0.25, 0.25]), - ]; - - // Ensure the duplicate UUID is introduced in the incremental insertion loop, - // not during initial simplex construction. - let dup_uuid = vertices[0].uuid(); - vertices[3].set_uuid(dup_uuid).unwrap(); - - let result: Result, (), (), 2>, _> = - DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices); + fn test_hilbert_dedup_empty_input() { + let vertices: Vec> = vec![]; + let result = order_vertices_hilbert(vertices, true); + assert!(result.is_empty(), "empty input must produce empty output"); + } - match result.unwrap_err() { - DelaunayTriangulationConstructionError::Triangulation( - DelaunayConstructionFailure::Tds { - reason: TdsConstructionFailure::DuplicateUuid { entity: _, uuid }, - }, - ) => { - assert_eq!(uuid, dup_uuid); - } - other => panic!("Expected DuplicateUuid error, got {other:?}"), - } + #[test] + fn test_hilbert_dedup_single_vertex() { + let vertices: Vec> = vec![vertex!([1.0, 2.0, 3.0])]; + let result = order_vertices_hilbert(vertices, true); + assert_eq!(result.len(), 1, "single vertex must be preserved"); } #[test] - fn test_validation_report_ok_for_valid_triangulation() { - init_tracing(); - let vertices = [ + fn test_hilbert_dedup_already_unique() { + // Distinct vertices — dedup should be a no-op. + let vertices: Vec> = 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::new(&vertices).unwrap(); - assert!(dt.validation_report().is_ok()); + let n = vertices.len(); + let result = order_vertices_hilbert(vertices, true); + assert_eq!(result.len(), n, "already-unique input must be unchanged"); } #[test] - fn test_validation_report_returns_mapping_failures_only() { + fn test_new_with_options_hilbert_smoke_3d() { init_tracing(); - let vertices = [ + let vertices: Vec> = 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]), + vertex!([0.25, 0.25, 0.25]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - - // Break UUID↔key mappings: remove one vertex UUID entry. - let uuid = dt.tri.tds.vertices().next().unwrap().1.uuid(); - dt.tri.tds.uuid_to_vertex_key.remove(&uuid); + let opts = ConstructionOptions::default() + .with_insertion_order(InsertionOrderStrategy::Hilbert) + .with_retry_policy(RetryPolicy::Disabled); - let report = dt.validation_report().unwrap_err(); - assert!(!report.violations.is_empty()); - assert!(report.violations.iter().all(|v| { - matches!( - v.kind, - InvariantKind::VertexMappings | InvariantKind::SimplexMappings - ) - })); + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new_with_options(&vertices, opts).unwrap(); - // Early-return on mapping failures: do not add derived invariants. - assert!( - report - .violations - .iter() - .all(|v| v.kind != InvariantKind::DelaunayProperty) - ); + assert_eq!(dt.number_of_vertices(), 5); + assert!(dt.validate().is_ok()); } #[test] - fn test_validation_report_includes_vertex_incidence_violation() { + fn test_new_with_options_shuffled_retry_policy_smoke_3d() { init_tracing(); - let vertices = [ + let vertices: Vec> = 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]), + vertex!([0.25, 0.25, 0.25]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); + let opts = ConstructionOptions::default() + .with_insertion_order(InsertionOrderStrategy::Input) + .with_retry_policy(RetryPolicy::Shuffled { + attempts: NonZeroUsize::new(2).unwrap(), + base_seed: Some(123), + }); - // Corrupt a `Vertex::incident_simplex` pointer. - let vertex_key = dt.tri.tds.vertices().next().unwrap().0; - dt.tri - .tds - .vertex_mut(vertex_key) - .unwrap() - .set_incident_simplex(Some(SimplexKey::default())); + let dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new_with_options(&vertices, opts).unwrap(); - let report = dt.validation_report().unwrap_err(); - assert!( - report - .violations - .iter() - .any(|v| v.kind == InvariantKind::VertexIncidence) - ); + assert_eq!(dt.number_of_vertices(), 5); + assert!(dt.validate().is_ok()); } #[test] - fn test_serde_roundtrip_uses_custom_deserialize_impl() { + fn test_new_with_options_smoke_3d() { init_tracing(); - let vertices = [ + let vertices: Vec> = 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 opts = ConstructionOptions::default().with_retry_policy(RetryPolicy::Disabled); let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - - let json = serde_json::to_string(&dt).unwrap(); - - // AdaptiveKernel: no auto-Deserialize impl, use try_from_tds. - let tds: Tds = serde_json::from_str(&json).unwrap(); - let roundtrip_adaptive = - DelaunayTriangulation::try_from_tds(tds, AdaptiveKernel::new()).unwrap(); - - assert_eq!( - roundtrip_adaptive.number_of_vertices(), - dt.number_of_vertices() - ); - assert_eq!( - roundtrip_adaptive.number_of_simplices(), - dt.number_of_simplices() - ); - - // `insertion_state.last_inserted_simplex` is a performance-only locate hint and is intentionally not - // persisted across serde round-trips (it is reset to `None` in `from_tds`). - assert!( - roundtrip_adaptive - .insertion_state - .last_inserted_simplex - .is_none() - ); - - // RobustKernel: has a custom Deserialize impl. - let roundtrip_robust: DelaunayTriangulation, (), (), 3> = - serde_json::from_str(&json).unwrap(); + DelaunayTriangulation::new_with_options(&vertices, opts).unwrap(); - assert_eq!( - roundtrip_robust.number_of_vertices(), - dt.number_of_vertices() - ); - assert_eq!( - roundtrip_robust.number_of_simplices(), - dt.number_of_simplices() - ); - assert!( - roundtrip_robust - .insertion_state - .last_inserted_simplex - .is_none() - ); + assert_eq!(dt.number_of_vertices(), 4); + assert_eq!(dt.number_of_simplices(), 1); + assert!(dt.validate().is_ok()); } - // ========================================================================= - // Topology traversal forwarding tests (DelaunayTriangulation → Triangulation) - // ========================================================================= - #[test] - fn test_topology_traversal_methods_are_forwarded() { + fn test_new_with_construction_statistics_counts_initial_simplex_3d() { init_tracing(); - // Single tetrahedron: 4 vertices, 1 simplex, 6 unique edges. - let vertices = vec![ + let vertices: Vec> = 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::new(&vertices).unwrap(); - let tri = dt.as_triangulation(); - - let edges_dt: HashSet<_> = dt.edges().collect(); - let edges_tri: HashSet<_> = tri.edges().collect(); - assert_eq!(edges_dt, edges_tri); - assert_eq!(edges_dt.len(), 6); - - let index = dt.build_adjacency_index().unwrap(); - let edges_dt_index: HashSet<_> = dt.edges_with_index(&index).collect(); - let edges_tri_index: HashSet<_> = tri.edges_with_index(&index).collect(); - assert_eq!(edges_dt_index, edges_tri_index); - assert_eq!(edges_dt_index, edges_dt); - - let v0 = dt.vertices().next().unwrap().0; - let incident_dt: HashSet<_> = dt.incident_edges(v0).collect(); - let incident_tri: HashSet<_> = tri.incident_edges(v0).collect(); - assert_eq!(incident_dt, incident_tri); - assert_eq!(incident_dt.len(), 3); - - let incident_dt_index: HashSet<_> = dt.incident_edges_with_index(&index, v0).collect(); - let incident_tri_index: HashSet<_> = tri.incident_edges_with_index(&index, v0).collect(); - assert_eq!(incident_dt_index, incident_tri_index); - assert_eq!(incident_dt_index, incident_dt); - - let simplex_key = dt.simplices().next().unwrap().0; - let neighbors_dt: Vec<_> = dt.simplex_neighbors(simplex_key).collect(); - let neighbors_tri: Vec<_> = tri.simplex_neighbors(simplex_key).collect(); - assert_eq!(neighbors_dt, neighbors_tri); - assert!(neighbors_dt.is_empty()); - - let neighbors_dt_index: Vec<_> = dt - .simplex_neighbors_with_index(&index, simplex_key) - .collect(); - let neighbors_tri_index: Vec<_> = tri - .simplex_neighbors_with_index(&index, simplex_key) - .collect(); - assert_eq!(neighbors_dt_index, neighbors_tri_index); - assert_eq!(neighbors_dt_index, neighbors_dt); - - // Geometry/topology accessors should be forwarded as well. - let simplex_vertices_dt = dt.simplex_vertices(simplex_key).unwrap(); - let simplex_vertices_tri = tri.simplex_vertices(simplex_key).unwrap(); - assert_eq!(simplex_vertices_dt, simplex_vertices_tri); - assert_eq!(simplex_vertices_dt.len(), 4); - - let coords_dt = dt.vertex_coords(v0).unwrap(); - let coords_tri = tri.vertex_coords(v0).unwrap(); - assert_eq!(coords_dt, coords_tri); + let (dt, stats) = + DelaunayTriangulation::new_with_construction_statistics(&vertices).unwrap(); - // Missing keys should behave the same as on `Triangulation`. - assert!(dt.vertex_coords(VertexKey::default()).is_none()); - assert!(dt.simplex_vertices(SimplexKey::default()).is_none()); + assert_eq!(dt.number_of_vertices(), 4); + assert_eq!(stats.inserted, 4); + assert_eq!(stats.total_skipped(), 0); + assert_eq!(stats.total_attempts, 4); + assert_eq!(stats.max_attempts, 1); + assert_eq!(stats.attempts_histogram.get(1).copied().unwrap_or(0), 4); } - // ========================================================================= - // Tests for per-insertion incremental Delaunay repair during batch construction - // ========================================================================= - - /// Verifies that 3D batch construction with more vertices than the initial simplex - /// (D+1 = 4) completes successfully. Each inserted vertex triggers a - /// `maybe_repair_after_insertion` call (`EveryInsertion` policy) in - /// `insert_remaining_vertices_seeded`. #[test] - fn test_batch_3d_construction_with_extra_vertex_triggers_incremental_repair() { + fn test_new_with_options_and_construction_statistics_skips_duplicate_3d() { init_tracing(); - let vertices = vec![ + let vertices: Vec> = 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]), - // 5th vertex: triggers one iteration of insert_remaining_vertices_seeded - vertex!([0.3, 0.3, 0.3]), - ]; - let dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - assert_eq!(dt.number_of_vertices(), 5); - assert!(dt.validate().is_ok()); - } - - /// Verifies the `Some`-stats call site: `new_with_construction_statistics` passes - /// `Some(&mut stats)` into `insert_remaining_vertices_seeded`, exercising the - /// per-insertion repair in the stats-collecting branch. - #[test] - fn test_batch_3d_construction_statistics_with_extra_vertex_triggers_incremental_repair() { - init_tracing(); - 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]), - vertex!([0.3, 0.3, 0.3]), ]; + let duplicate_uuid = vertices[4].uuid(); + + let opts = ConstructionOptions::default() + .with_insertion_order(InsertionOrderStrategy::Input) + .with_retry_policy(RetryPolicy::Disabled); + let (dt, stats) = - DelaunayTriangulation::<_, (), (), 3>::new_with_construction_statistics(&vertices) + DelaunayTriangulation::new_with_options_and_construction_statistics(&vertices, opts) .unwrap(); - assert_eq!(dt.number_of_vertices(), 5); - assert_eq!(stats.inserted, 5); - assert!(dt.validate().is_ok()); + + assert_eq!(dt.number_of_vertices(), 4); + assert_eq!(stats.inserted, 4); + assert_eq!(stats.skipped_duplicate, 1); + assert_eq!(stats.skipped_degeneracy, 0); + assert_eq!(stats.total_skipped(), 1); + assert_eq!(stats.total_attempts, 5); + assert_eq!(stats.attempts_histogram.get(1).copied().unwrap_or(0), 5); + + assert_eq!(stats.skip_samples.len(), 1); + let sample = &stats.skip_samples[0]; + assert_eq!(sample.index, 4); + assert_eq!(sample.uuid, duplicate_uuid); + assert_eq!(sample.coords, vec![0.0, 0.0, 0.0]); + assert!(sample.coords_available); + assert_eq!(sample.attempts, 1); + assert!(sample.error.contains("Duplicate coordinates")); } - /// Forced local repair non-convergence exercises the D>=4 soft-fail branch - /// without relying on a fragile floating-point cycling fixture. #[test] - fn test_batch_4d_forced_nonconvergent_local_repair_canonicalizes_without_stats() { + fn test_new_with_topology_guarantee_sets_pl() { init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0, 0.0]), - vertex!([0.0, 1.0, 0.0, 0.0]), - vertex!([0.0, 0.0, 1.0, 0.0]), - vertex!([0.0, 0.0, 0.0, 1.0]), - vertex!([0.2, 0.2, 0.2, 0.2]), - vertex!([0.35, 0.25, 0.15, 0.3]), + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), ]; - let _guard = ForceRepairNonconvergentGuard::enable(); - let kernel = RobustKernel::::new(); - let dt = - DelaunayTriangulation::, (), (), 4>::with_kernel(&kernel, &vertices) - .expect( - "D>=4 construction should continue after forced local repair non-convergence", - ); + let dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new_with_topology_guarantee( + &vertices, + TopologyGuarantee::PLManifold, + ) + .unwrap(); - assert_eq!(dt.number_of_vertices(), vertices.len()); - assert!(dt.validate().is_ok()); + assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); } - /// The statistics path has a separate insertion loop, so it needs its own - /// forced D>=4 local-repair non-convergence assertion. #[test] - fn test_batch_4d_forced_nonconvergent_local_repair_canonicalizes_with_stats() { + fn test_empty_creates_empty_triangulation() { init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0, 0.0]), - vertex!([0.0, 1.0, 0.0, 0.0]), - vertex!([0.0, 0.0, 1.0, 0.0]), - vertex!([0.0, 0.0, 0.0, 1.0]), - vertex!([0.2, 0.2, 0.2, 0.2]), - vertex!([0.35, 0.25, 0.15, 0.3]), - ]; - - let _guard = ForceRepairNonconvergentGuard::enable(); - let kernel = RobustKernel::::new(); - let (dt, stats) = - DelaunayTriangulation::, (), (), 4>::with_options_and_statistics( - &kernel, - &vertices, - TopologyGuarantee::DEFAULT, - ConstructionOptions::default(), - ) - .expect( - "D>=4 stats construction should continue after forced local repair non-convergence", - ); + let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); - assert_eq!(dt.number_of_vertices(), vertices.len()); - assert_eq!(stats.inserted, vertices.len()); - assert!(dt.validate().is_ok()); + assert_eq!(dt.number_of_vertices(), 0); + assert_eq!(dt.number_of_simplices(), 0); + assert_eq!(dt.dim(), -1); } - /// Exercises the `EveryN` cadence through the full bulk path: vertices - /// accumulate `pending_repair_seeds`, trigger cadenced local repair, and - /// then complete through `finalize_bulk_construction`. #[test] - fn test_batch_4d_every_n_repair_cadence_runs_with_pending_seeds() { + fn test_empty_supports_incremental_insertion() { init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0, 0.0, 0.0]), - vertex!([1.0, 0.0, 0.0, 0.0]), - vertex!([0.0, 1.0, 0.0, 0.0]), - vertex!([0.0, 0.0, 1.0, 0.0]), - vertex!([0.0, 0.0, 0.0, 1.0]), - vertex!([0.2, 0.2, 0.2, 0.2]), - vertex!([0.35, 0.25, 0.15, 0.3]), - ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); + assert_eq!(dt.number_of_vertices(), 0); - test_hooks::reset_batch_local_repair_calls(); - let _guard = ForceRepairNonconvergentGuard::enable(); - let kernel = RobustKernel::::new(); - let options = ConstructionOptions::default() - .with_batch_repair_policy(DelaunayRepairPolicy::EveryN(NonZeroUsize::new(2).unwrap())); - let (dt, stats) = - DelaunayTriangulation::, (), (), 4>::with_options_and_statistics( - &kernel, - &vertices, - TopologyGuarantee::DEFAULT, - options, - ) - .expect("EveryN batch repair should soft-fail forced local non-convergence and finish"); + dt.insert(vertex!([0.0, 0.0])).unwrap(); + dt.insert(vertex!([1.0, 0.0])).unwrap(); + assert_eq!(dt.number_of_simplices(), 0); - assert_eq!(dt.number_of_vertices(), vertices.len()); - assert_eq!(stats.inserted, vertices.len()); - assert_eq!( - test_hooks::batch_local_repair_calls(), - 1, - "EveryN(2) should run one cadenced repair before finalize_bulk_construction" - ); - assert!(dt.validate().is_ok()); + dt.insert(vertex!([0.0, 1.0])).unwrap(); + assert_eq!(dt.number_of_simplices(), 1); } #[test] + #[expect( + clippy::too_many_lines, + reason = "single classification table keeps soft-fail and hard-error mapping cases together" + )] fn test_repair_soft_fail_classification() { - let nonconvergent = test_hooks::synthetic_nonconvergent_error(); + let nonconvergent = DelaunayRepairError::NonConvergent { + max_flips: 1000, + 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: 1, + queue_order: RepairQueueOrder::Fifo, + }), + }; assert!(TestDelaunay::<4>::can_soft_fail(&nonconvergent)); let postcondition = DelaunayRepairError::PostconditionFailed { @@ -11717,8 +6385,7 @@ mod tests { let error = InsertionError::TopologyValidation(TdsError::InconsistentDataStructure { message: "missing simplex".to_string(), }); - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!( matches!( mapped, @@ -11740,8 +6407,7 @@ mod tests { message: "det=0".to_string(), }, )); - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!( matches!( mapped, @@ -11763,8 +6429,7 @@ mod tests { message: "det<0 after canonicalization".to_string(), }, )); - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!( matches!( mapped, @@ -11784,12 +6449,11 @@ mod tests { let error = InsertionError::TopologyValidationFailed { message: "test".to_string(), source: TriangulationValidationError::IsolatedVertex { - vertex_key: VertexKey::from(slotmap::KeyData::from_ffi(1)), + vertex_key: VertexKey::from(KeyData::from_ffi(1)), vertex_uuid: Uuid::nil(), }, }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!( matches!( mapped, @@ -11809,8 +6473,7 @@ mod tests { classification: TopologyClassification::Ball(3), }, }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!( matches!( mapped, @@ -11825,8 +6488,7 @@ mod tests { let error = InsertionError::CavityFilling { reason: CavityFillingError::EmptyFanTriangulation, }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!(matches!( mapped, TriangulationConstructionError::InternalInconsistency { .. } @@ -11837,11 +6499,10 @@ mod tests { fn test_map_orientation_canonicalization_error_neighbor_wiring_is_internal() { let error = InsertionError::NeighborWiring { reason: NeighborWiringError::MissingSimplex { - simplex_key: SimplexKey::from(slotmap::KeyData::from_ffi(1)), + simplex_key: SimplexKey::from(KeyData::from_ffi(1)), }, }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!(matches!( mapped, TriangulationConstructionError::InternalInconsistency { .. } @@ -11854,8 +6515,7 @@ mod tests { entity: EntityKind::Simplex, uuid: Uuid::nil(), }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!(matches!( mapped, TriangulationConstructionError::InternalInconsistency { .. } @@ -11890,8 +6550,7 @@ mod tests { ]; for error in geometry_errors { let label = format!("{error}"); - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!( matches!( mapped, @@ -11913,8 +6572,7 @@ mod tests { }), context: DelaunayRepairFailureContext::OrientationCanonicalization, }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!( matches!( mapped, @@ -11926,15 +6584,12 @@ mod tests { ); } - // ---- map_insertion_error tests ---- - #[test] fn test_map_insertion_error_cavity_filling() { let error = InsertionError::CavityFilling { reason: CavityFillingError::EmptyFanTriangulation, }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(error); + let mapped = TestDelaunay::<3>::map_insertion_error(error); assert!( matches!( mapped, @@ -11950,11 +6605,10 @@ mod tests { fn test_map_insertion_error_neighbor_wiring() { let error = InsertionError::NeighborWiring { reason: NeighborWiringError::MissingSimplex { - simplex_key: SimplexKey::from(slotmap::KeyData::from_ffi(1)), + simplex_key: SimplexKey::from(KeyData::from_ffi(1)), }, }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(error); + let mapped = TestDelaunay::<3>::map_insertion_error(error); assert!( matches!( mapped, @@ -11969,8 +6623,7 @@ mod tests { let error = InsertionError::TopologyValidation(TdsError::InconsistentDataStructure { message: "broken".to_string(), }); - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(error); + let mapped = TestDelaunay::<3>::map_insertion_error(error); assert!( matches!(mapped, TriangulationConstructionError::Tds(_)), "TopologyValidation should map to Tds(ValidationError), got: {mapped:?}" @@ -11983,8 +6636,7 @@ mod tests { entity: EntityKind::Simplex, uuid: Uuid::nil(), }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(error); + let mapped = TestDelaunay::<3>::map_insertion_error(error); assert!( matches!(mapped, TriangulationConstructionError::Tds(_)), "DuplicateUuid should map to Tds(DuplicateUuid), got: {mapped:?}" @@ -11996,8 +6648,7 @@ mod tests { let error = InsertionError::DuplicateCoordinates { coordinates: "[1,2,3]".to_string(), }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(error); + let mapped = TestDelaunay::<3>::map_insertion_error(error); assert!( matches!( mapped, @@ -12012,10 +6663,9 @@ mod tests { let conflict = InsertionError::ConflictRegion(ConflictError::OpenBoundary { facet_count: 2, ridge_vertex_count: 1, - open_simplex: SimplexKey::from(slotmap::KeyData::from_ffi(1)), + open_simplex: SimplexKey::from(KeyData::from_ffi(1)), }); - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(conflict); + let mapped = TestDelaunay::<3>::map_insertion_error(conflict); assert!(matches!( mapped, TriangulationConstructionError::InsertionConflictRegion { @@ -12024,301 +6674,91 @@ mod tests { )); let location = InsertionError::Location(LocateError::EmptyTriangulation); - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(location); + let mapped = TestDelaunay::<3>::map_insertion_error(location); assert!(matches!( mapped, TriangulationConstructionError::InsertionLocation { source: LocateError::EmptyTriangulation, } - )); - - let non_manifold = InsertionError::NonManifoldTopology { - facet_hash: 0xab, - simplex_count: 3, - }; - let mapped = DelaunayTriangulation::, (), (), 3>::map_insertion_error( - non_manifold, - ); - assert!(matches!( - mapped, - TriangulationConstructionError::InsertionNonManifoldTopology { - facet_hash: 0xab, - simplex_count: 3, - } - )); - - let hull = InsertionError::HullExtension { - reason: HullExtensionReason::NoVisibleFacets, - }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(hull); - assert!(matches!( - mapped, - TriangulationConstructionError::InsertionHullExtension { - reason: HullExtensionReason::NoVisibleFacets, - } - )); - - let delaunay = InsertionError::DelaunayValidationFailed { - source: DelaunayTriangulationValidationError::VerificationFailed { - message: "test".to_string(), - }, - }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(delaunay); - assert!(matches!( - mapped, - TriangulationConstructionError::InsertionDelaunayValidation { .. } - )); - - let topology = InsertionError::TopologyValidationFailed { - message: "test".to_string(), - source: TriangulationValidationError::EulerCharacteristicMismatch { - computed: 3, - expected: 2, - classification: TopologyClassification::Ball(3), - }, - }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(topology); - assert!(matches!( - mapped, - TriangulationConstructionError::InsertionTopologyValidation { .. } - )); - } - - #[test] - fn test_map_insertion_error_hard_repair_is_internal() { - let error = InsertionError::DelaunayRepairFailed { - source: Box::new(DelaunayRepairError::Flip(FlipError::UnsupportedDimension { - dimension: 1, - })), - context: DelaunayRepairFailureContext::LocalRepair, - }; - let mapped = - DelaunayTriangulation::, (), (), 3>::map_insertion_error(error); - assert!( - matches!( - mapped, - TriangulationConstructionError::InternalInconsistency { ref message } - if message.contains("local repair") - && message.contains("Bistellar flip not supported for D=1") - ), - "hard repair errors during insertion should be internal: {mapped:?}" - ); - } - - // ---- is_retryable refinement tests ---- - - #[test] - fn test_is_retryable_degenerate_orientation_is_retryable() { - let error = InsertionError::TopologyValidation(TdsError::Geometric( - GeometricError::DegenerateOrientation { - message: "det=0".to_string(), - }, - )); - assert!( - error.is_retryable(), - "DegenerateOrientation should be retryable" - ); - } - - #[test] - fn test_is_retryable_negative_orientation_is_retryable() { - let error = InsertionError::TopologyValidation(TdsError::Geometric( - GeometricError::NegativeOrientation { - message: "det<0".to_string(), - }, - )); - assert!( - error.is_retryable(), - "NegativeOrientation should be retryable" - ); - } - - #[test] - fn test_is_retryable_isolated_vertex_is_retryable() { - let error = InsertionError::TopologyValidationFailed { - message: "test".to_string(), - source: TriangulationValidationError::IsolatedVertex { - vertex_key: VertexKey::from(slotmap::KeyData::from_ffi(1)), - vertex_uuid: Uuid::nil(), - }, - }; - assert!( - error.is_retryable(), - "IsolatedVertex should be retryable (geometry-sensitive conflict region)" - ); - } - - #[test] - fn test_is_retryable_inconsistent_data_structure_is_not_retryable() { - let error = InsertionError::TopologyValidation(TdsError::InconsistentDataStructure { - message: "missing simplex".to_string(), - }); - assert!( - !error.is_retryable(), - "InconsistentDataStructure should NOT be retryable" - ); - } - - #[test] - fn test_is_retryable_failed_to_create_simplex_is_not_retryable() { - let error = InsertionError::TopologyValidation(TdsError::FailedToCreateSimplex { - message: "test".to_string(), - }); - assert!( - !error.is_retryable(), - "FailedToCreateSimplex should NOT be retryable" - ); - } - - // ---- VerificationFailed variant tests ---- - - #[test] - fn test_verification_failed_display() { - let err = DelaunayTriangulationValidationError::VerificationFailed { - message: "flip predicate detected non-Delaunay facet".to_string(), - }; - let msg = err.to_string(); - assert!( - msg.contains("Delaunay verification failed"), - "Display should contain prefix: {msg}" - ); - assert!( - msg.contains("flip predicate detected non-Delaunay facet"), - "Display should contain inner message: {msg}" - ); - } - - #[test] - fn repair_operation_failed_preserves_source() { - let source = DelaunayRepairError::NonConvergent { - max_flips: 7, - diagnostics: Box::new(DelaunayRepairDiagnostics { - facets_checked: 3, - flips_performed: 7, - max_queue_len: 5, - 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, - }), - }; - let err = DelaunayTriangulationValidationError::RepairOperationFailed { - operation: DelaunayRepairOperation::VertexRemoval, - source: Box::new(source), - }; - - let msg = err.to_string(); - assert!(msg.contains("vertex removal")); - match &err { - DelaunayTriangulationValidationError::RepairOperationFailed { - operation: DelaunayRepairOperation::VertexRemoval, - source, - } if matches!( - source.as_ref(), - DelaunayRepairError::NonConvergent { max_flips: 7, .. } - ) => {} - other => panic!("expected typed vertex-removal repair source, got {other:?}"), - } - let chained = err - .source() - .expect("typed repair failure should expose source error") - .to_string(); - assert!(chained.contains("failed to converge after 7 flips")); - } + )); - #[test] - fn test_delaunay_validation_error_tds_variant_display() { - let inner = TdsError::InconsistentDataStructure { - message: "broken link".to_string(), + let non_manifold = InsertionError::NonManifoldTopology { + facet_hash: 0xab, + simplex_count: 3, }; - let err = DelaunayTriangulationValidationError::from(inner); - assert!(err.to_string().contains("broken link")); - } + let mapped = TestDelaunay::<3>::map_insertion_error(non_manifold); + assert!(matches!( + mapped, + TriangulationConstructionError::InsertionNonManifoldTopology { + facet_hash: 0xab, + simplex_count: 3, + } + )); - #[test] - fn test_delaunay_validation_error_triangulation_variant_display() { - let inner = TriangulationValidationError::IsolatedVertex { - vertex_key: VertexKey::from(slotmap::KeyData::from_ffi(1)), - vertex_uuid: Uuid::nil(), + let hull = InsertionError::HullExtension { + reason: HullExtensionReason::NoVisibleFacets, }; - let err = DelaunayTriangulationValidationError::from(inner); - assert!(err.to_string().contains("Isolated vertex")); - } - - // ---- DT validate() error-mapping tests ---- - - #[test] - fn test_dt_validate_maps_tds_error_to_tds_variant() { - init_tracing(); - let vertices: Vec> = 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 mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); + let mapped = TestDelaunay::<3>::map_insertion_error(hull); + assert!(matches!( + mapped, + TriangulationConstructionError::InsertionHullExtension { + reason: HullExtensionReason::NoVisibleFacets, + } + )); - // 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); + let delaunay = InsertionError::DelaunayValidationFailed { + source: DelaunayTriangulationValidationError::VerificationFailed { + message: "test".to_string(), + }, + }; + let mapped = TestDelaunay::<3>::map_insertion_error(delaunay); + assert!(matches!( + mapped, + TriangulationConstructionError::InsertionDelaunayValidation { .. } + )); - match dt.validate() { - Err(DelaunayTriangulationValidationError::Tds(source)) - if matches!(source.as_ref(), TdsError::MappingInconsistency { .. }) => {} - other => panic!( - "Expected DelaunayTriangulationValidationError::Tds(MappingInconsistency), got {other:?}" - ), - } + let topology = InsertionError::TopologyValidationFailed { + message: "test".to_string(), + source: TriangulationValidationError::EulerCharacteristicMismatch { + computed: 3, + expected: 2, + classification: TopologyClassification::Ball(3), + }, + }; + let mapped = TestDelaunay::<3>::map_insertion_error(topology); + assert!(matches!( + mapped, + TriangulationConstructionError::InsertionTopologyValidation { .. } + )); } #[test] - fn test_dt_validate_maps_topology_error_to_triangulation_variant() { - init_tracing(); - let vertices: Vec> = 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 mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); - - // Add an isolated vertex so Level 3 (topology) fails. - let _ = dt - .tds_mut() - .insert_vertex_with_mapping(vertex!([0.5, 0.5, 0.5])) - .unwrap(); - - match dt.validate() { - Err(DelaunayTriangulationValidationError::Triangulation(source)) - if matches!( - source.as_ref(), - TriangulationValidationError::IsolatedVertex { .. } - ) => {} - other => panic!( - "Expected DelaunayTriangulationValidationError::Triangulation(IsolatedVertex), got {other:?}" + fn test_map_insertion_error_hard_repair_is_internal() { + let error = InsertionError::DelaunayRepairFailed { + source: Box::new(DelaunayRepairError::Flip(FlipError::UnsupportedDimension { + dimension: 1, + })), + context: DelaunayRepairFailureContext::LocalRepair, + }; + let mapped = TestDelaunay::<3>::map_insertion_error(error); + assert!( + matches!( + mapped, + TriangulationConstructionError::InternalInconsistency { ref message } + if message.contains("local repair") + && message.contains("Bistellar flip not supported for D=1") ), - } + "hard repair errors during insertion should be internal: {mapped:?}" + ); } - // ---- map_orientation_canonicalization_error: OrientationViolation ---- - #[test] fn test_map_orientation_canonicalization_error_orientation_violation_is_internal_inconsistency() { let error = InsertionError::TopologyValidation(TdsError::OrientationViolation { - simplex1_key: SimplexKey::from(slotmap::KeyData::from_ffi(1)), + simplex1_key: SimplexKey::from(KeyData::from_ffi(1)), simplex1_uuid: Uuid::nil(), - simplex2_key: SimplexKey::from(slotmap::KeyData::from_ffi(2)), + simplex2_key: SimplexKey::from(KeyData::from_ffi(2)), simplex2_uuid: Uuid::nil(), simplex1_facet_index: 0, simplex2_facet_index: 1, @@ -12327,8 +6767,7 @@ mod tests { observed_odd_permutation: true, expected_odd_permutation: false, }); - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!( matches!( mapped, @@ -12338,16 +6777,13 @@ mod tests { ); } - // ---- map_orientation_canonicalization_error: ConflictRegion ---- - #[test] fn test_map_orientation_canonicalization_error_conflict_region_is_degeneracy() { let error = InsertionError::ConflictRegion(ConflictError::NonManifoldFacet { facet_hash: 0x123, simplex_count: 3, }); - let mapped = - DelaunayTriangulation::, (), (), 3>::map_orientation_canonicalization_error(error); + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); assert!( matches!( mapped, @@ -12357,32 +6793,6 @@ mod tests { ); } - // ---- RetryPolicy default regression test (#306) ---- - - /// Verify that `RetryPolicy::default()` returns `Shuffled` with the expected - /// attempt count in all build profiles. Previously the default was `Disabled` - /// in release builds, causing #306. - #[test] - fn test_retry_policy_default_is_shuffled_in_all_profiles() { - let policy = RetryPolicy::default(); - match policy { - RetryPolicy::Shuffled { - attempts, - base_seed, - } => { - assert_eq!( - attempts.get(), - DELAUNAY_SHUFFLE_ATTEMPTS, - "default retry attempts should equal DELAUNAY_SHUFFLE_ATTEMPTS" - ); - assert_eq!(base_seed, None, "default base_seed should be None"); - } - other => panic!("RetryPolicy::default() should be Shuffled, got {other:?}"), - } - } - - // ---- is_non_retryable_construction_error tests ---- - #[test] fn test_is_non_retryable_construction_error_duplicate_uuid() { let err: DelaunayTriangulationConstructionError = @@ -12392,9 +6802,7 @@ mod tests { }) .into(); assert!( - DelaunayTriangulation::, (), (), 3>::is_non_retryable_construction_error( - &err - ), + TestDelaunay::<3>::is_non_retryable_construction_error(&err), "DuplicateUuid should be non-retryable" ); } @@ -12407,9 +6815,7 @@ mod tests { } .into(); assert!( - DelaunayTriangulation::, (), (), 3>::is_non_retryable_construction_error( - &err - ), + TestDelaunay::<3>::is_non_retryable_construction_error(&err), "InternalInconsistency should be non-retryable" ); } @@ -12423,9 +6829,7 @@ mod tests { ) .into(); assert!( - DelaunayTriangulation::, (), (), 3>::is_non_retryable_construction_error( - &err - ), + TestDelaunay::<3>::is_non_retryable_construction_error(&err), "TDS validation failures should be non-retryable" ); } @@ -12456,15 +6860,11 @@ mod tests { .into(); assert!( - DelaunayTriangulation::, (), (), 3>::is_non_retryable_construction_error( - &insertion_err - ), + TestDelaunay::<3>::is_non_retryable_construction_error(&insertion_err), "InsertionTopologyValidation should be non-retryable" ); assert!( - DelaunayTriangulation::, (), (), 3>::is_non_retryable_construction_error( - &final_err - ), + TestDelaunay::<3>::is_non_retryable_construction_error(&final_err), "FinalTopologyValidation should be non-retryable" ); } @@ -12477,72 +6877,157 @@ mod tests { } .into(); assert!( - !DelaunayTriangulation::, (), (), 3>::is_non_retryable_construction_error( - &err - ), + !TestDelaunay::<3>::is_non_retryable_construction_error(&err), "GeometricDegeneracy should NOT be non-retryable" ); } - // ---- advanced repair fallback-chain error context tests ---- - - /// Verify that the `HeuristicRebuildFailed` error from - /// `repair_delaunay_with_flips_advanced` includes the full fallback - /// chain context (primary, robust, and heuristic failures) when all - /// three stages fail. #[test] - fn test_advanced_repair_fallback_error_preserves_full_chain_context() { - // Construct the error exactly the way `repair_delaunay_with_flips_advanced` - // builds it when all three stages fail. - let primary_err = DelaunayRepairError::NonConvergent { - max_flips: 1000, - diagnostics: Box::new(DelaunayRepairDiagnostics { - facets_checked: 50, - flips_performed: 1000, - max_queue_len: 42, - 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, - }), - }; - let robust_err = DelaunayRepairError::PostconditionFailed { - message: "robust postcondition failure".to_string(), - }; - let heuristic_inner = DelaunayRepairError::HeuristicRebuildFailed { - message: "heuristic rebuild failed after 3 attempts: attempt 3/3 (shuffle_seed=1 perturbation_seed=2): inner".to_string(), - }; + fn retry_policy_default_is_shuffled_in_all_profiles() { + let policy = RetryPolicy::default(); + match policy { + RetryPolicy::Shuffled { + attempts, + base_seed, + } => { + assert_eq!(attempts.get(), DELAUNAY_SHUFFLE_ATTEMPTS); + assert_eq!(base_seed, None); + } + other => panic!("RetryPolicy::default() should be Shuffled, got {other:?}"), + } + } + + macro_rules! gen_local_repair_flip_budget_tests { + ($dim:literal, $floor:ident, $factor:ident) => { + pastey::paste! { + #[test] + fn []() { + assert_eq!(local_repair_flip_budget::<$dim>(0), $floor); - // Simulate the map_err closure in repair_delaunay_with_flips_advanced. - let heuristic_message = match heuristic_inner { - DelaunayRepairError::HeuristicRebuildFailed { message } => message, - other => other.to_string(), + let seed_count = 10; + let raw = seed_count * ($dim + 1) * $factor; + assert_eq!(local_repair_flip_budget::<$dim>(seed_count), raw.max($floor)); + } + } }; - let combined = DelaunayRepairError::HeuristicRebuildFailed { - message: format!( - "primary repair failed ({primary_err}); robust fallback failed ({robust_err}); {heuristic_message}" + } + + gen_local_repair_flip_budget_tests!( + 2, + LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_LT_4, + LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_LT_4 + ); + gen_local_repair_flip_budget_tests!( + 3, + LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_LT_4, + LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_LT_4 + ); + gen_local_repair_flip_budget_tests!( + 4, + LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_GE_4, + LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_GE_4 + ); + gen_local_repair_flip_budget_tests!( + 5, + LOCAL_REPAIR_FLIP_BUDGET_FLOOR_D_GE_4, + LOCAL_REPAIR_FLIP_BUDGET_FACTOR_D_GE_4 + ); + + #[test] + fn local_repair_seed_backlog_threshold_uses_dimension_regimes() { + assert_eq!( + local_repair_seed_backlog_threshold::<3>(), + 4 * LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_LT_4 + ); + assert_eq!( + local_repair_seed_backlog_threshold::<4>(), + 5 * LOCAL_REPAIR_SEED_BACKLOG_FACTOR_D_GE_4 + ); + } + + #[test] + fn batch_local_repair_trigger_prefers_cadence_over_backlog() { + let policy = DelaunayRepairPolicy::EveryN(NonZeroUsize::new(4).unwrap()); + let threshold = local_repair_seed_backlog_threshold::<3>(); + + assert_eq!( + batch_local_repair_trigger::<3>(policy, 4, TopologyGuarantee::PLManifold, threshold), + Some(BatchLocalRepairTrigger::Cadence) + ); + } + + #[test] + fn batch_local_repair_trigger_runs_every_insertion_below_backlog() { + assert_eq!( + batch_local_repair_trigger::<3>( + DelaunayRepairPolicy::EveryInsertion, + 1, + TopologyGuarantee::PLManifold, + 1, ), - }; + Some(BatchLocalRepairTrigger::Cadence) + ); + assert_eq!( + batch_local_repair_trigger::<3>( + DelaunayRepairPolicy::EveryInsertion, + 0, + TopologyGuarantee::PLManifold, + 1, + ), + None + ); + } - let msg = combined.to_string(); - assert!( - msg.contains("primary repair failed"), - "error should mention primary failure: {msg}" + #[test] + fn batch_local_repair_trigger_repairs_early_on_seed_backlog() { + let policy = DelaunayRepairPolicy::EveryN(NonZeroUsize::new(128).unwrap()); + let threshold = local_repair_seed_backlog_threshold::<3>(); + + assert_eq!( + batch_local_repair_trigger::<3>(policy, 7, TopologyGuarantee::PLManifold, threshold), + Some(BatchLocalRepairTrigger::SeedBacklog) ); - assert!( - msg.contains("robust fallback failed"), - "error should mention robust failure: {msg}" + assert_eq!( + batch_local_repair_trigger::<3>( + policy, + 7, + TopologyGuarantee::PLManifold, + threshold - 1 + ), + None ); - assert!( - msg.contains("robust postcondition failure"), - "error should include robust failure details: {msg}" + } + + #[test] + fn batch_local_repair_trigger_respects_policy_and_topology() { + let threshold = local_repair_seed_backlog_threshold::<3>(); + + assert_eq!( + batch_local_repair_trigger::<3>( + DelaunayRepairPolicy::Never, + 7, + TopologyGuarantee::PLManifold, + threshold + ), + None ); - assert!( - msg.contains("heuristic rebuild failed after 3 attempts"), - "error should include heuristic rebuild details: {msg}" + assert_eq!( + batch_local_repair_trigger::<3>( + DelaunayRepairPolicy::EveryN(NonZeroUsize::new(128).unwrap()), + 7, + TopologyGuarantee::PLManifold, + 0 + ), + None + ); + assert_eq!( + batch_local_repair_trigger::<3>( + DelaunayRepairPolicy::EveryN(NonZeroUsize::new(128).unwrap()), + 7, + TopologyGuarantee::Pseudomanifold, + threshold + ), + Some(BatchLocalRepairTrigger::SeedBacklog) ); } } diff --git a/src/triangulation/delaunayize.rs b/src/delaunay/delaunayize.rs similarity index 97% rename from src/triangulation/delaunayize.rs rename to src/delaunay/delaunayize.rs index 4468b0af..1f6bdb51 100644 --- a/src/triangulation/delaunayize.rs +++ b/src/delaunay/delaunayize.rs @@ -3,7 +3,7 @@ //! This module provides `delaunayize_by_flips`, a single public entrypoint that //! takes an existing [`DelaunayTriangulation`], performs bounded deterministic //! topology repair toward -//! [`TopologyGuarantee::PLManifold`](crate::triangulation::TopologyGuarantee::PLManifold), +//! [`TopologyGuarantee::PLManifold`](crate::TopologyGuarantee::PLManifold), //! and then applies //! standard flip-based Delaunay repair. //! @@ -20,19 +20,21 @@ //! # Example //! //! ```rust -//! use delaunay::prelude::triangulation::delaunayize::*; +//! use delaunay::prelude::delaunayize::*; //! +//! # fn main() -> Result<(), Box> { //! 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 mut dt: DelaunayTriangulation<_, (), (), 3> = -//! DelaunayTriangulation::new(&vertices).unwrap(); +//! let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; //! -//! let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap(); +//! let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default())?; //! assert!(outcome.topology_repair.succeeded); +//! # Ok(()) +//! # } //! ``` //! //! # Explicitly Deferred @@ -44,10 +46,10 @@ // Re-export outcome/error field types so users can name the public contract // without reaching into lower-level modules. +pub use crate::construction::DelaunayTriangulationConstructionError; +pub use crate::flips::{DelaunayRepairError, DelaunayRepairStats}; pub use crate::tds::SimplexValidationError; -pub use crate::triangulation::delaunay::DelaunayTriangulationConstructionError; -pub use crate::triangulation::flips::{DelaunayRepairError, DelaunayRepairStats}; -pub use crate::triangulation::{PlManifoldRepairError, PlManifoldRepairStats}; +pub use crate::{PlManifoldRepairError, PlManifoldRepairStats}; #[cfg(test)] use crate::core::algorithms::flips::{DelaunayRepairDiagnostics, RepairQueueOrder}; @@ -61,7 +63,8 @@ use crate::core::traits::data_type::DataType; use crate::core::vertex::Vertex; use crate::geometry::kernel::{ExactPredicates, Kernel}; use crate::geometry::traits::coordinate::CoordinateScalar; -use crate::triangulation::delaunay::{DelaunayRepairHeuristicConfig, DelaunayTriangulation}; +use crate::repair::DelaunayRepairHeuristicConfig; +use crate::triangulation::DelaunayTriangulation; use thiserror::Error; #[cfg(test)] @@ -127,7 +130,7 @@ mod test_hooks { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::delaunayize::DelaunayizeConfig; +/// use delaunay::prelude::delaunayize::DelaunayizeConfig; /// /// let config = DelaunayizeConfig::default(); /// assert_eq!(config.topology_max_iterations, 64); @@ -176,20 +179,22 @@ impl Default for DelaunayizeConfig { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::delaunayize::*; +/// use delaunay::prelude::delaunayize::*; /// +/// # fn main() -> Result<(), Box> { /// 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 mut dt: DelaunayTriangulation<_, (), (), 3> = -/// DelaunayTriangulation::new(&vertices).unwrap(); +/// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// -/// let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap(); +/// let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default())?; /// assert!(outcome.topology_repair.succeeded); /// assert!(!outcome.used_fallback_rebuild); +/// # Ok(()) +/// # } /// ``` #[derive(Debug, Clone)] #[non_exhaustive] @@ -242,7 +247,7 @@ pub struct DelaunayizeOutcome { /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::delaunayize::*; +/// use delaunay::prelude::delaunayize::*; /// /// let err = DelaunayizeError::DelaunayRepairFailed { /// source: DelaunayRepairError::PostconditionFailed { @@ -587,19 +592,21 @@ where /// # Examples /// /// ```rust -/// use delaunay::prelude::triangulation::delaunayize::*; +/// use delaunay::prelude::delaunayize::*; /// +/// # fn main() -> Result<(), Box> { /// 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 mut dt: DelaunayTriangulation<_, (), (), 3> = -/// DelaunayTriangulation::new(&vertices).unwrap(); +/// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// -/// let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap(); +/// let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default())?; /// assert!(outcome.topology_repair.succeeded); +/// # Ok(()) +/// # } /// ``` #[expect( clippy::result_large_err, @@ -719,8 +726,8 @@ mod tests { use crate::geometry::point::Point; use crate::geometry::traits::coordinate::Coordinate; use crate::tds::VertexKey; - use crate::triangulation::{DelaunayTriangulationBuilder, TriangulationConstructionError}; use crate::vertex; + use crate::{DelaunayTriangulationBuilder, TriangulationConstructionError}; use slotmap::KeyData; use std::error::Error as StdError; diff --git a/src/triangulation/diagnostics.rs b/src/delaunay/diagnostics.rs similarity index 99% rename from src/triangulation/diagnostics.rs rename to src/delaunay/diagnostics.rs index de4e2042..110fc18c 100644 --- a/src/triangulation/diagnostics.rs +++ b/src/delaunay/diagnostics.rs @@ -3,7 +3,7 @@ //! # Examples //! //! ```rust -//! use delaunay::prelude::triangulation::diagnostics::ConstructionTelemetry; +//! use delaunay::prelude::diagnostics::ConstructionTelemetry; //! //! let telemetry = ConstructionTelemetry::default(); //! assert!(!telemetry.has_data()); @@ -225,7 +225,7 @@ impl ConstructionTelemetry { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::diagnostics::ConstructionTelemetry; + /// use delaunay::prelude::diagnostics::ConstructionTelemetry; /// /// let mut telemetry = ConstructionTelemetry::default(); /// assert!(!telemetry.has_data()); diff --git a/src/triangulation/flips.rs b/src/delaunay/flips.rs similarity index 78% rename from src/triangulation/flips.rs rename to src/delaunay/flips.rs index 48df3a9a..875d9bd2 100644 --- a/src/triangulation/flips.rs +++ b/src/delaunay/flips.rs @@ -3,8 +3,8 @@ //! This module exposes **high-level** flip methods for explicit triangulation editing. //! These operations do **not** automatically restore the Delaunay property. //! For Delaunay construction/removal, use -//! [`crate::triangulation::delaunay::DelaunayTriangulation::insert`] and -//! [`crate::triangulation::delaunay::DelaunayTriangulation::remove_vertex`]. +//! [`crate::DelaunayTriangulation::insert`] and +//! [`crate::DelaunayTriangulation::remove_vertex`]. #![forbid(unsafe_code)] @@ -27,15 +27,16 @@ 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::delaunay::DelaunayTriangulation; +use crate::triangulation::DelaunayTriangulation; /// High-level triangulation editing operations via bistellar flips. /// /// # Example /// /// ```rust -/// use delaunay::prelude::triangulation::construction::TopologyGuarantee; -/// use delaunay::prelude::triangulation::flips::*; +/// use delaunay::prelude::construction::TopologyGuarantee; +/// use delaunay::prelude::flips::*; /// +/// # fn main() -> Result<(), Box> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -46,20 +47,25 @@ use crate::triangulation::delaunay::DelaunayTriangulation; /// DelaunayTriangulation::new_with_topology_guarantee( /// &vertices, /// TopologyGuarantee::PLManifold, -/// ) -/// .unwrap(); -/// let simplex_key = dt.simplices().next().unwrap().0; +/// )?; +/// let simplex_key = dt +/// .simplices() +/// .next() +/// .map(|(key, _)| key) +/// .ok_or_else(|| std::io::Error::other("empty triangulation"))?; /// /// // Split a simplex by inserting a vertex (k=1 move). -/// let _info = dt -/// .flip_k1_insert(simplex_key, vertex!([0.1, 0.1, 0.1])) -/// .unwrap(); +/// let _info = dt.flip_k1_insert(simplex_key, vertex!([0.1, 0.1, 0.1]))?; +/// # Ok(()) +/// # } /// ``` -pub trait BistellarFlips -where - K: Kernel, - U: DataType, -{ +pub trait BistellarFlips { + /// Coordinate scalar type used by vertices inserted through k=1 flips. + type Scalar; + + /// User data type stored on vertices inserted through k=1 flips. + type VertexData; + /// Apply a forward k=1 move (simplex split) by inserting a vertex into a simplex. /// /// # Errors @@ -70,9 +76,10 @@ where /// # Example /// /// ```rust - /// use delaunay::prelude::triangulation::construction::TopologyGuarantee; - /// use delaunay::prelude::triangulation::flips::*; + /// use delaunay::prelude::construction::TopologyGuarantee; + /// use delaunay::prelude::flips::*; /// + /// # fn main() -> Result<(), Box> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -83,18 +90,23 @@ where /// DelaunayTriangulation::new_with_topology_guarantee( /// &vertices, /// TopologyGuarantee::PLManifold, - /// ) - /// .unwrap(); - /// let simplex_key = dt.simplices().next().unwrap().0; + /// )?; + /// let simplex_key = dt + /// .simplices() + /// .next() + /// .map(|(key, _)| key) + /// .ok_or_else(|| std::io::Error::other("empty triangulation"))?; /// /// // Insert a vertex into the simplex - /// let info = dt.flip_k1_insert(simplex_key, vertex!([0.25, 0.25, 0.25])).unwrap(); + /// let info = dt.flip_k1_insert(simplex_key, vertex!([0.25, 0.25, 0.25]))?; /// assert!(!info.new_simplices.is_empty()); + /// # Ok(()) + /// # } /// ``` fn flip_k1_insert( &mut self, simplex_key: SimplexKey, - vertex: Vertex, + vertex: Vertex, ) -> Result, FlipError>; /// Apply an inverse k=1 move (vertex collapse). @@ -107,9 +119,10 @@ where /// # Example /// /// ```rust - /// use delaunay::prelude::triangulation::construction::TopologyGuarantee; - /// use delaunay::prelude::triangulation::flips::*; + /// use delaunay::prelude::construction::TopologyGuarantee; + /// use delaunay::prelude::flips::*; /// + /// # fn main() -> Result<(), Box> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -120,15 +133,20 @@ where /// DelaunayTriangulation::new_with_topology_guarantee( /// &vertices, /// TopologyGuarantee::PLManifold, - /// ) - /// .unwrap(); - /// let simplex_key = dt.simplices().next().unwrap().0; - /// let inserted = dt.flip_k1_insert(simplex_key, vertex!([0.25, 0.25, 0.25])).unwrap(); + /// )?; + /// let simplex_key = dt + /// .simplices() + /// .next() + /// .map(|(key, _)| key) + /// .ok_or_else(|| std::io::Error::other("empty triangulation"))?; + /// let inserted = dt.flip_k1_insert(simplex_key, vertex!([0.25, 0.25, 0.25]))?; /// let inserted_vertex = inserted.inserted_face_vertices[0]; /// /// // Remove the inserted vertex - /// let info = dt.flip_k1_remove(inserted_vertex).unwrap(); + /// let info = dt.flip_k1_remove(inserted_vertex)?; /// assert!(!info.removed_simplices.is_empty()); + /// # Ok(()) + /// # } /// ``` fn flip_k1_remove(&mut self, vertex_key: VertexKey) -> Result, FlipError>; @@ -142,8 +160,9 @@ where /// # Example /// /// ```rust - /// use delaunay::prelude::triangulation::flips::*; + /// use delaunay::prelude::flips::*; /// + /// # fn main() -> Result<(), Box> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -151,8 +170,7 @@ where /// vertex!([0.0, 0.0, 1.0]), /// vertex!([0.5, 0.5, 0.3]), /// ]; - /// let mut dt: DelaunayTriangulation<_, (), (), 3> = - /// DelaunayTriangulation::new(&vertices).unwrap(); + /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// /// // Find an interior facet and attempt a k=2 flip /// // Note: k=2 flips require specific geometric conditions @@ -167,6 +185,8 @@ where /// let _ = dt.flip_k2(facet); // May succeed or fail depending on configuration /// } /// } + /// # Ok(()) + /// # } /// ``` fn flip_k2(&mut self, facet: FacetHandle) -> Result, FlipError>; @@ -180,8 +200,9 @@ where /// # Example /// /// ```rust - /// use delaunay::prelude::triangulation::flips::*; + /// use delaunay::prelude::flips::*; /// + /// # fn main() -> Result<(), Box> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -189,12 +210,13 @@ where /// vertex!([0.0, 0.0, 1.0]), /// vertex!([1.0, 1.0, 1.0]), /// ]; - /// let mut dt: DelaunayTriangulation<_, (), (), 3> = - /// DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// /// // k=3 flips require specific ridge configurations in 3D and above /// // This is an illustrative example; actual ridge selection depends on topology /// let _ = dt; // Use dt to prevent unused variable warning + /// # Ok(()) + /// # } /// ``` fn flip_k3(&mut self, ridge: RidgeHandle) -> Result, FlipError>; @@ -220,12 +242,15 @@ where ) -> Result, FlipError>; } -impl BistellarFlips for Triangulation +impl BistellarFlips for Triangulation where K: Kernel, U: DataType, V: DataType, { + type Scalar = K::Scalar; + type VertexData = U; + fn flip_k1_insert( &mut self, simplex_key: SimplexKey, @@ -273,12 +298,15 @@ where } } -impl BistellarFlips for DelaunayTriangulation +impl BistellarFlips for DelaunayTriangulation where K: Kernel, U: DataType, V: DataType, { + type Scalar = K::Scalar; + type VertexData = U; + fn flip_k1_insert( &mut self, simplex_key: SimplexKey, @@ -339,8 +367,9 @@ where mod tests { use super::*; + use crate::TopologyGuarantee; + use crate::core::collections::spatial_hash_grid::HashGridIndex; use crate::geometry::kernel::{AdaptiveKernel, FastKernel}; - use crate::triangulation::TopologyGuarantee; use crate::vertex; use slotmap::KeyData; @@ -373,6 +402,33 @@ mod tests { assert!(tri.validate().is_ok()); } + #[test] + fn flip_k1_insert_invalidates_caches() { + let vertices: Vec> = 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 mut dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + + let simplex_key = dt.simplices().next().unwrap().0; + dt.insertion_state.last_inserted_simplex = Some(simplex_key); + let mut spatial_index = HashGridIndex::::new(1.0); + for (vertex_key, vertex) in dt.vertices() { + spatial_index.insert_vertex(vertex_key, vertex.point().coords()); + } + dt.spatial_index = Some(spatial_index); + + dt.flip_k1_insert(simplex_key, vertex!([0.2, 0.2, 0.2])) + .unwrap(); + + assert!(dt.insertion_state.last_inserted_simplex.is_none()); + assert!(dt.spatial_index.is_none()); + assert!(dt.as_triangulation().validate().is_ok()); + } + #[test] fn triangulation_flip_k2_rejects_invalid_facet_index() { let vertices = vec![ diff --git a/src/delaunay/insertion.rs b/src/delaunay/insertion.rs new file mode 100644 index 00000000..4e7450f7 --- /dev/null +++ b/src/delaunay/insertion.rs @@ -0,0 +1,1335 @@ +//! Incremental insertion and vertex-removal operations for Delaunay triangulations. +//! +//! This module owns post-construction mutation APIs: inserting vertices, removing +//! vertices, maintaining insertion caches, and running policy-controlled local +//! Delaunay repair after those mutations. + +#![forbid(unsafe_code)] + +#[cfg(test)] +use crate::construction::test_hooks; +use crate::core::algorithms::flips::{ + DelaunayRepairError, DelaunayRepairRun, FlipError, apply_bistellar_flip_k1_inverse, + repair_delaunay_with_flips_k2_k3, repair_delaunay_with_flips_k2_k3_run, +}; +use crate::core::algorithms::incremental_insertion::{ + DelaunayRepairErrorSummary, DelaunayRepairFailureContext, InsertionError, +}; +use crate::core::collections::spatial_hash_grid::HashGridIndex; +use crate::core::collections::{FastHashSet, SimplexKeyBuffer}; +use crate::core::operations::{InsertionOutcome, InsertionStatistics}; +use crate::core::tds::{InvariantError, NeighborValidationError, SimplexKey, TdsError, VertexKey}; +use crate::core::traits::data_type::DataType; +use crate::core::validation::{ + TopologyGuarantee, TriangulationValidationError, insertion_error_to_invariant_error, +}; +use crate::core::vertex::Vertex; +use crate::geometry::kernel::Kernel; +use crate::geometry::traits::coordinate::CoordinateScalar; +use crate::repair::{DelaunayRepairOperation, DelaunayRepairPolicy}; +use crate::topology::manifold::{ManifoldError, validate_ridge_links_for_simplices}; +use crate::triangulation::DelaunayTriangulation; +use crate::validation::DelaunayTriangulationValidationError; +use num_traits::NumCast; +use std::env; + +const RIDGE_LINK_REPAIR_VALIDATION_MESSAGE: &str = "Topology invalid after Delaunay repair"; + +fn ridge_link_repair_validation_error(err: ManifoldError) -> InsertionError { + match TriangulationValidationError::try_from(err) { + Ok(source) => InsertionError::TopologyValidationFailed { + message: RIDGE_LINK_REPAIR_VALIDATION_MESSAGE.to_string(), + source, + }, + Err(source) => InsertionError::TopologyValidation(source), + } +} + +// ============================================================================= +// MUTATION (Requires Numeric Scalar Bounds) +// ============================================================================= +// +// Incremental insertion, removal, and post-insertion repair/check helpers. +// These require `NumCast` for spatial-index construction, Triangulation-layer +// insertion, and Triangulation-layer removal. `Kernel` already guarantees +// `CoordinateScalar`. + +impl DelaunayTriangulation +where + K: Kernel, + K::Scalar: NumCast, + U: DataType, + V: DataType, +{ + /// Lazily seeds the spatial index from existing vertices so incremental + /// insertion can start from deserialized or manually constructed TDS state. + fn ensure_spatial_index_seeded(&mut self) { + if self.spatial_index.is_some() { + return; + } + + let duplicate_tolerance: K::Scalar = + ::from(1e-10_f64).unwrap_or_else(K::Scalar::default_tolerance); + let mut index: HashGridIndex = HashGridIndex::new(duplicate_tolerance); + + for (vkey, vertex) in self.tri.tds.vertices() { + index.insert_vertex(vkey, vertex.point().coords()); + } + + self.spatial_index = Some(index); + } + + /// Insert a vertex into the Delaunay triangulation using incremental cavity-based algorithm. + /// + /// This method handles all stages of triangulation construction: + /// - **Bootstrap (< D+1 vertices)**: Accumulates vertices without creating simplices + /// - **Initial simplex (D+1 vertices)**: Automatically builds the first D-simplex + /// - **Incremental (> D+1 vertices)**: Uses cavity-based insertion with point location + /// + /// # Algorithm + /// 1. Insert vertex into Tds + /// 2. Check vertex count: + /// - If < D+1: Return (bootstrap phase) + /// - If == D+1: Build initial simplex from all vertices + /// - If > D+1: Continue with steps 3-7 + /// 3. Locate simplex containing the point + /// 4. Find conflict region (simplices whose circumspheres contain the point) + /// 5. Extract cavity boundary + /// 6. Fill cavity (create new simplices) + /// 7. Wire neighbors locally + /// 8. Remove conflict simplices + /// + /// # Errors + /// Returns error if: + /// - Duplicate UUID detected + /// - Initial simplex construction fails (when reaching D+1 vertices) + /// - Point is on a facet, edge, or vertex (degenerate cases not yet implemented) + /// - Conflict region computation fails + /// - Cavity boundary extraction fails + /// - Cavity filling or neighbor wiring fails + /// + /// Note: Points outside the convex hull are handled automatically via hull extension. + /// + /// # Examples + /// + /// Incremental insertion from empty triangulation: + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// // Start with empty triangulation + /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); + /// assert_eq!(dt.number_of_vertices(), 0); + /// assert_eq!(dt.number_of_simplices(), 0); + /// + /// // Insert vertices one by one - bootstrap phase (no simplices yet) + /// dt.insert(vertex!([0.0, 0.0, 0.0])).unwrap(); + /// dt.insert(vertex!([1.0, 0.0, 0.0])).unwrap(); + /// dt.insert(vertex!([0.0, 1.0, 0.0])).unwrap(); + /// assert_eq!(dt.number_of_vertices(), 3); + /// assert_eq!(dt.number_of_simplices(), 0); // Still no simplices + /// + /// // 4th vertex triggers initial simplex creation + /// dt.insert(vertex!([0.0, 0.0, 1.0])).unwrap(); + /// assert_eq!(dt.number_of_vertices(), 4); + /// assert_eq!(dt.number_of_simplices(), 1); // First simplex created! + /// + /// // Further insertions use cavity-based algorithm + /// dt.insert(vertex!([0.2, 0.2, 0.2])).unwrap(); + /// assert_eq!(dt.number_of_vertices(), 5); + /// assert!(dt.number_of_simplices() > 1); + /// ``` + /// + /// Using batch construction (traditional approach): + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// // Create initial triangulation with 5 vertices (4-simplex) + /// let vertices = vec![ + /// vertex!([0.0, 0.0, 0.0, 0.0]), + /// vertex!([1.0, 0.0, 0.0, 0.0]), + /// vertex!([0.0, 1.0, 0.0, 0.0]), + /// vertex!([0.0, 0.0, 1.0, 0.0]), + /// vertex!([0.0, 0.0, 0.0, 1.0]), + /// ]; + /// let mut dt: DelaunayTriangulation<_, (), (), 4> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// assert_eq!(dt.number_of_vertices(), 5); + /// + /// // Insert additional interior vertex + /// dt.insert(vertex!([0.2, 0.2, 0.2, 0.2])).unwrap(); + /// assert_eq!(dt.number_of_vertices(), 6); + /// assert!(dt.number_of_simplices() > 1); + /// ``` + pub fn insert(&mut self, vertex: Vertex) -> Result { + self.ensure_spatial_index_seeded(); + + // Fully delegate to Triangulation layer + // Triangulation handles: + // - Manifold maintenance (conflict simplices, cavity, repairs) + // - Bootstrap and initial simplex + // - Location and conflict region computation + // + // DelaunayTriangulation adds: + // - Kernel (provides in-sphere predicate for Delaunay property) + // - Hint caching for performance + // - Future: Delaunay property restoration after removal + // + // Transactional guard: post-steps (flip repair and/or global Delaunay checks) can fail. + // If they do, rollback to leave the triangulation unchanged. + let next_insertion_count = self + .insertion_state + .delaunay_repair_insertion_count + .saturating_add(1); + let could_have_simplices_after_insertion = self.tri.tds.number_of_simplices() > 0 + || self.tri.tds.number_of_vertices().saturating_add(1) > D; + let snapshot_needed = could_have_simplices_after_insertion + && (self.insertion_state.delaunay_repair_policy != DelaunayRepairPolicy::Never + || self + .insertion_state + .delaunay_check_policy + .should_check(next_insertion_count)); + let snapshot = + snapshot_needed.then(|| (self.tri.tds.clone_for_rollback(), self.insertion_state)); + + let insertion_result = (|| { + let hint = self.insertion_state.last_inserted_simplex; + let insert_detail = { + let (tri, spatial_index) = (&mut self.tri, &mut self.spatial_index); + tri.insert_with_statistics_seeded_indexed_detailed( + vertex, + None, + hint, + 0, + spatial_index.as_mut(), + None, + )? + }; + let repair_seed_simplices = insert_detail.repair_seed_simplices; + let delaunay_repair_required = insert_detail.delaunay_repair_required; + + match insert_detail.outcome { + InsertionOutcome::Inserted { + vertex_key: v_key, + hint, + } => { + self.insertion_state.last_inserted_simplex = hint; + self.insertion_state.delaunay_repair_insertion_count = self + .insertion_state + .delaunay_repair_insertion_count + .saturating_add(1); + if delaunay_repair_required { + self.maybe_repair_after_insertion(v_key, hint, &repair_seed_simplices)?; + } + self.maybe_check_after_insertion()?; + Ok(v_key) + } + InsertionOutcome::Skipped { error } => Err(error), + } + })(); + + match insertion_result { + Ok(v_key) => Ok(v_key), + Err(err) => { + if let Some((tds, insertion_state)) = snapshot { + self.spatial_index = None; + self.tri.tds = tds; + self.insertion_state = insertion_state; + } + Err(err) + } + } + } + + /// Insert a vertex and return the insertion outcome plus statistics. + /// + /// This is a convenience wrapper around the triangulation-layer insertion-with-statistics + /// implementation that also updates the internal `insertion_state.last_inserted_simplex` hint cache. + /// + /// # Errors + /// + /// Returns `Err(InsertionError)` only for non-retryable structural failures. + /// Retryable geometric degeneracies that exhaust all attempts return + /// `Ok((InsertionOutcome::Skipped { .. }, stats))`. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::insertion::InsertionOutcome; + /// + /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); + /// + /// let (outcome, stats) = dt + /// .insert_with_statistics(vertex!([0.0, 0.0, 0.0])) + /// .unwrap(); + /// + /// assert!(stats.success()); + /// assert!(matches!(outcome, InsertionOutcome::Inserted { .. })); + /// ``` + pub fn insert_with_statistics( + &mut self, + vertex: Vertex, + ) -> Result<(InsertionOutcome, InsertionStatistics), InsertionError> { + self.ensure_spatial_index_seeded(); + + // Transactional guard: post-steps (flip repair and/or global Delaunay checks) can fail. + // If they do, rollback to leave the triangulation unchanged. + let next_insertion_count = self + .insertion_state + .delaunay_repair_insertion_count + .saturating_add(1); + let could_have_simplices_after_insertion = self.tri.tds.number_of_simplices() > 0 + || self.tri.tds.number_of_vertices().saturating_add(1) > D; + let snapshot_needed = could_have_simplices_after_insertion + && (self.insertion_state.delaunay_repair_policy != DelaunayRepairPolicy::Never + || self + .insertion_state + .delaunay_check_policy + .should_check(next_insertion_count)); + let snapshot = + snapshot_needed.then(|| (self.tri.tds.clone_for_rollback(), self.insertion_state)); + + let insertion_result = (|| { + let hint = self.insertion_state.last_inserted_simplex; + let insert_detail = { + let (tri, spatial_index) = (&mut self.tri, &mut self.spatial_index); + tri.insert_with_statistics_seeded_indexed_detailed( + vertex, + None, + hint, + 0, + spatial_index.as_mut(), + None, + )? + }; + let stats = insert_detail.stats; + let repair_seed_simplices = insert_detail.repair_seed_simplices; + let delaunay_repair_required = insert_detail.delaunay_repair_required; + + let outcome = match insert_detail.outcome { + InsertionOutcome::Inserted { vertex_key, hint } => { + self.insertion_state.last_inserted_simplex = hint; + self.insertion_state.delaunay_repair_insertion_count = self + .insertion_state + .delaunay_repair_insertion_count + .saturating_add(1); + if delaunay_repair_required { + self.maybe_repair_after_insertion( + vertex_key, + hint, + &repair_seed_simplices, + )?; + } + self.maybe_check_after_insertion()?; + InsertionOutcome::Inserted { vertex_key, hint } + } + other @ InsertionOutcome::Skipped { .. } => other, + }; + + Ok((outcome, stats)) + })(); + + match insertion_result { + Ok((outcome, stats)) => Ok((outcome, stats)), + Err(err) => { + if let Some((tds, insertion_state)) = snapshot { + self.spatial_index = None; + self.tri.tds = tds; + self.insertion_state = insertion_state; + } + Err(err) + } + } + } + + /// Keeps the default insertion path on the same repair helper as capped + /// debug and heuristic paths. + fn maybe_repair_after_insertion( + &mut self, + vertex_key: VertexKey, + hint: Option, + extra_seed_simplices: &[SimplexKey], + ) -> Result<(), InsertionError> { + self.maybe_repair_after_insertion_capped(vertex_key, hint, extra_seed_simplices, None) + } + + /// Like [`maybe_repair_after_insertion`](Self::maybe_repair_after_insertion) but + /// forwards an optional per-attempt flip cap to the underlying repair functions. + /// + /// `extra_seed_simplices` widens the local repair frontier beyond the inserted vertex + /// star. This is used when cavity reduction shrinks simplices out of the conflict + /// region: those simplices stay in the triangulation and may still need a local + /// Delaunay revisit even though they are no longer adjacent to the new vertex. + pub(crate) fn maybe_repair_after_insertion_capped( + &mut self, + vertex_key: VertexKey, + hint: Option, + extra_seed_simplices: &[SimplexKey], + max_flips: Option, + ) -> Result<(), InsertionError> { + let topology = self.tri.topology_guarantee(); + if !self.should_run_delaunay_repair_for( + topology, + self.insertion_state.delaunay_repair_insertion_count, + ) { + return Ok(()); + } + + // Prefer the merged local frontier when we have one; otherwise fall back to the + // validated locate hint so repair can still start from the inserted star. + let seed_simplices = + self.collect_local_repair_seed_simplices(vertex_key, extra_seed_simplices); + let hint_seed = hint.and_then(|ck| { + if !self.tri.tds.contains_simplex(ck) { + if env::var_os("DELAUNAY_REPAIR_TRACE").is_some() { + tracing::debug!( + "[repair] insertion seed hint missing (simplex={ck:?}, vertex={vertex_key:?})" + ); + } + return None; + } + + let contains_vertex = self + .tri + .tds + .simplex(ck) + .is_some_and(|simplex| simplex.contains_vertex(vertex_key)); + if !contains_vertex && env::var_os("DELAUNAY_REPAIR_TRACE").is_some() { + tracing::debug!( + "[repair] insertion seed hint does not contain vertex (simplex={ck:?}, vertex={vertex_key:?})" + ); + } + + contains_vertex.then_some(ck) + }); + + let seed_ref = if seed_simplices.is_empty() { + hint_seed.as_ref().map(std::slice::from_ref) + } else { + Some(seed_simplices.as_slice()) + }; + + let repair_result = { + self.invalidate_locate_hint_cache(); + let (tds, kernel) = (&mut self.tri.tds, &self.tri.kernel); + repair_delaunay_with_flips_k2_k3_run(tds, kernel, seed_ref, topology, max_flips) + }; + + #[cfg(test)] + let repair_result = if test_hooks::force_repair_nonconvergent_enabled() { + Err(test_hooks::synthetic_nonconvergent_error()) + } else { + repair_result + }; + + match repair_result { + Ok(run) => { + self.validate_ridge_links_after_repair(topology, &run)?; + } + Err( + e @ (DelaunayRepairError::NonConvergent { .. } + | DelaunayRepairError::PostconditionFailed { .. }), + ) => { + // Robust fallback: retry with `RobustKernel` which guarantees exact + // predicate evaluation. This covers 99.9%+ of repair failures. + // + // If the robust pass also fails, return an error. Callers that need + // the full heuristic rebuild (shuffled re-insertion) can invoke + // `repair_delaunay_with_flips_advanced()` explicitly. + let robust_run = self + .repair_delaunay_with_flips_robust_run(seed_ref, max_flips) + .map_err(|robust_err| InsertionError::DelaunayRepairFailed { + source: Box::new(robust_err), + context: DelaunayRepairFailureContext::LocalRepairRobustFallback { + initial: DelaunayRepairErrorSummary::from(&e), + }, + })?; + self.validate_ridge_links_after_repair(topology, &robust_run)?; + } + Err(e) => { + return Err(InsertionError::DelaunayRepairFailed { + source: Box::new(e), + context: DelaunayRepairFailureContext::LocalRepairNonRecoverable, + }); + } + } + + // Flip-based repair mutates simplex orderings; restore canonical positive geometric + // orientation before exposing the updated triangulation state. + self.tri.normalize_and_promote_positive_orientation()?; + self.tri + .validate_geometric_simplex_orientation() + .map_err(InsertionError::TopologyValidation)?; + Ok(()) + } + + /// Validates PL ridge links after a repair pass that actually performed flips. + /// + /// Ridge-link topology only changes where flips created replacement simplices, + /// so validation follows that mutation frontier even if the repair queues + /// were seeded from the full triangulation. If a repair reports flips + /// without a mutation frontier, fall back to a full simplex list defensively. + fn validate_ridge_links_after_repair( + &self, + topology: TopologyGuarantee, + run: &DelaunayRepairRun, + ) -> Result<(), InsertionError> { + if !topology.requires_ridge_links() || run.stats.flips_performed == 0 { + return Ok(()); + } + + let validate_simplices = |simplices: &[SimplexKey]| { + if simplices.is_empty() { + return Ok(()); + } + validate_ridge_links_for_simplices(&self.tri.tds, simplices) + .map_err(ridge_link_repair_validation_error) + }; + + if !run.touched_simplices.is_empty() { + if run.used_full_reseed && env::var_os("DELAUNAY_REPAIR_TRACE").is_some() { + tracing::debug!( + "[repair] validating ridge links on {} flip-created simplices after full reseed", + run.touched_simplices.len() + ); + } + return validate_simplices(&run.touched_simplices); + } + + let validation_simplices: Vec = self.tri.tds.simplex_keys().collect(); + validate_simplices(&validation_simplices) + } + + /// Merge the inserted vertex star with any simplices that cavity reduction touched and + /// left in place. Stale simplices are ignored so callers can pass raw cavity-trace sets. + fn collect_local_repair_seed_simplices( + &self, + vertex_key: VertexKey, + extra_seed_simplices: &[SimplexKey], + ) -> Vec { + let mut seen: FastHashSet = FastHashSet::default(); + let mut seed_simplices = Vec::new(); + + // Keep the inserted vertex star first because it is the hottest local region and + // the best chance of fixing ordinary post-insertion violations cheaply. + for simplex_key in self.tri.adjacent_simplices(vertex_key) { + if seen.insert(simplex_key) { + seed_simplices.push(simplex_key); + } + } + + // Then widen the frontier with simplices touched by cavity shaping that survived in + // the triangulation; deduping here lets callers pass raw trace buffers safely. + for &simplex_key in extra_seed_simplices { + if self.tri.tds.contains_simplex(simplex_key) && seen.insert(simplex_key) { + seed_simplices.push(simplex_key); + } + } + + seed_simplices + } + + /// Runs policy-controlled global validation after insertion so expensive + /// Delaunay checks stay opt-in for incremental workflows. + pub(crate) fn maybe_check_after_insertion(&self) -> Result<(), InsertionError> { + if self.tri.tds.number_of_simplices() == 0 { + return Ok(()); + } + + let policy = self.insertion_state.delaunay_check_policy; + let insertion_count = self.insertion_state.delaunay_repair_insertion_count; + if !policy.should_check(insertion_count) { + return Ok(()); + } + + self.is_valid() + .map_err(|e| InsertionError::DelaunayValidationFailed { source: e }) + } + + /// Removes a vertex and retriangulates the resulting cavity using fan triangulation. + /// + /// This operation delegates to `Triangulation::remove_vertex()` which: + /// 1. Finds all simplices containing the vertex + /// 2. Removes those simplices (creating a cavity) + /// 3. Fills the cavity with fan triangulation + /// 4. Wires neighbors and rebuilds vertex-simplex incidence + /// 5. Removes the vertex + /// + /// Fast-path: if the vertex star is a simplex (exactly D+1 incident simplices with + /// consistent adjacency), this method collapses it via the **inverse k=1** bistellar + /// flip. Otherwise it falls back to fan triangulation. + /// + /// The triangulation remains topologically valid after removal. However, both the + /// inverse k=1 fast-path and fan triangulation may temporarily violate the Delaunay + /// property in some cases. If the [`DelaunayRepairPolicy`] allows it, a flip-based + /// repair pass is run automatically after removal. + /// + /// The post-removal repair and orientation canonicalization steps are + /// transactional: if either step fails, this method restores the triangulation + /// and insertion state to their pre-removal state before returning the error. + /// The spatial index is retained across rollback because its keys are + /// validated against the live TDS before use. On successful removal, + /// topology-dependent locate hints are invalidated and the removed vertex key + /// is pruned from the spatial index. + /// + /// **Future Enhancement**: Delaunay-aware cavity retriangulation will be added for + /// removals. For now, occasional Delaunay violations after removal are expected and + /// can be addressed by running flip-based repair (e.g., [`repair_delaunay_with_flips`](Self::repair_delaunay_with_flips)) + /// or by leaving automatic repair enabled via [`DelaunayRepairPolicy`]. + /// + /// # Arguments + /// + /// * `vertex_key` - Key of the vertex to remove + /// + /// # Returns + /// + /// The number of simplices that were removed along with the vertex. Returns `Ok(0)` if + /// `vertex_key` does not refer to a vertex in the triangulation (e.g. a stale key from + /// a previously removed vertex or a key that was never inserted). This is a successful + /// no-op, not an error. + /// + /// # Errors + /// + /// Returns [`InvariantError`] if: + /// - The inverse k=1 flip encounters a neighbor-wiring failure (`InvariantError::Tds`). + /// - Fan retriangulation fails (`InvariantError::Tds`). + /// - Delaunay flip-based repair fails after removal + /// (`InvariantError::Delaunay(DelaunayTriangulationValidationError::RepairOperationFailed { .. })`). + /// - Orientation canonicalization fails after repair (`InvariantError::Tds`). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// let interior = vertex!([0.3, 0.3]); + /// let interior_uuid = interior.uuid(); + /// let vertices = [ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// interior, + /// ]; + /// let mut dt = DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// // Find the key of a known interior vertex. + /// let vertex_key = dt + /// .vertices() + /// .find(|(_, v)| v.uuid() == interior_uuid) + /// .map(|(k, _)| k) + /// .unwrap(); + /// + /// // Remove the vertex and all simplices containing it + /// let simplices_removed = dt.remove_vertex(vertex_key).unwrap(); + /// println!("Removed {} simplices along with the vertex", simplices_removed); + /// + /// // Vertex removal preserves topology; automatic repair is attempted when enabled. + /// assert!(dt.as_triangulation().validate().is_ok()); + /// ``` + pub fn remove_vertex(&mut self, vertex_key: VertexKey) -> Result { + let Some(removed_vertex) = self.tri.tds.vertex(vertex_key) else { + return Ok(0); + }; + let removed_vertex_coords = *removed_vertex.point().coords(); + let snapshot = (self.tri.tds.clone_for_rollback(), self.insertion_state); + + let result = (|| { + // 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 self.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: Box::new(reason), + }, + } + .into()); + } + Err(_) => self.tri.remove_vertex(vertex_key)?, + }; + + let topology = self.tri.topology_guarantee(); + if self.should_run_delaunay_repair_after_mutation(topology) { + let seed_ref = seed_simplices.as_deref(); + let repair_result = { + self.invalidate_locate_hint_cache(); + let (tds, kernel) = (&mut self.tri.tds, &self.tri.kernel); + repair_delaunay_with_flips_k2_k3(tds, kernel, seed_ref, topology, None) + }; + + #[cfg(test)] + let repair_result = if test_hooks::force_repair_nonconvergent_enabled() { + Err(test_hooks::synthetic_nonconvergent_error()) + } else { + repair_result + }; + + repair_result.map_err(|source| { + InvariantError::Delaunay( + DelaunayTriangulationValidationError::RepairOperationFailed { + operation: DelaunayRepairOperation::VertexRemoval, + source: Box::new(source), + }, + ) + })?; + + // Re-canonicalize geometric orientation (#258): flip repair may leave + // the global sign negative. + self.tri + .normalize_and_promote_positive_orientation() + .map_err(|e| { + insertion_error_to_invariant_error( + e, + "Orientation canonicalization failed after vertex removal", + ) + })?; + } + + Ok(simplices_removed) + })(); + + match result { + Ok(simplices_removed) => { + self.insertion_state.last_inserted_simplex = None; + if let Some(index) = self.spatial_index.as_mut() { + index.remove_vertex(&vertex_key, &removed_vertex_coords); + } + Ok(simplices_removed) + } + Err(err) => { + let (tds, insertion_state) = snapshot; + self.tri.tds = tds; + self.insertion_state = insertion_state; + Err(err) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::algorithms::flips::DelaunayRepairStats; + use crate::core::simplex::Simplex; + use crate::core::tds::Tds; + use crate::flips::BistellarFlips; + use crate::geometry::kernel::{AdaptiveKernel, RobustKernel}; + use crate::geometry::util::safe_usize_to_scalar; + use crate::vertex; + use slotmap::KeyData; + use std::sync::Once; + use uuid::Uuid; + + fn init_tracing() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_test_writer() + .try_init(); + }); + } + + struct ForceRepairNonconvergentGuard { + previous: bool, + } + + impl ForceRepairNonconvergentGuard { + fn enable() -> Self { + Self { + previous: test_hooks::set_force_repair_nonconvergent(true), + } + } + } + + impl Drop for ForceRepairNonconvergentGuard { + fn drop(&mut self) { + let _ = test_hooks::set_force_repair_nonconvergent(self.previous); + } + } + + fn simplex_vertices() -> Vec> { + let mut vertices = Vec::with_capacity(D + 1); + vertices.push(vertex!([0.0; D])); + for axis in 0..D { + let mut coords = [0.0; D]; + coords[axis] = 1.0; + vertices.push(vertex!(coords)); + } + vertices + } + + fn interior_vertex_for_k1_insert() -> Vertex { + let denominator = safe_usize_to_scalar::(D + 2) + .expect("D + 2 should convert exactly for rollback test dimensions"); + let coord = 1.0 / denominator; + vertex!([coord; D]) + } + + fn rollback_probe_vertex(point_index: usize) -> Vertex { + let dimension = + safe_usize_to_scalar::(D).expect("test dimensions should convert exactly"); + let point_index_scalar = + safe_usize_to_scalar::(point_index).expect("point index should convert exactly"); + let mut coords = [0.2 / dimension; D]; + let axis = point_index % D; + coords[axis] += point_index_scalar.mul_add(0.005, 0.02); + vertex!(coords) + } + + fn incident_simplex_count( + dt: &DelaunayTriangulation, (), (), D>, + vertex_key: VertexKey, + ) -> usize { + dt.simplices() + .filter(|(_, simplex)| simplex.vertices().contains(&vertex_key)) + .count() + } + + fn assert_forced_remove_vertex_rolls_back( + dt: &mut DelaunayTriangulation, (), (), D>, + vertex_key: VertexKey, + inserted_uuid: Uuid, + ) { + let vertex_count_before = dt.number_of_vertices(); + let simplex_count_before = dt.number_of_simplices(); + let hint_simplex_before = dt.simplices().next().map(|(key, _)| key); + dt.insertion_state.last_inserted_simplex = hint_simplex_before; + let mut spatial_index = HashGridIndex::::new(1.0); + for (vertex_key, vertex) in dt.vertices() { + spatial_index.insert_vertex(vertex_key, vertex.point().coords()); + } + dt.spatial_index = Some(spatial_index); + let last_inserted_simplex_before = dt.insertion_state.last_inserted_simplex; + let spatial_index_before = dt + .spatial_index + .as_ref() + .map(HashGridIndex::::debug_snapshot); + + let _guard = ForceRepairNonconvergentGuard::enable(); + let result = dt.remove_vertex(vertex_key); + let err = result.expect_err("forced repair failure should make removal fail"); + match err { + InvariantError::Delaunay( + DelaunayTriangulationValidationError::RepairOperationFailed { + operation: DelaunayRepairOperation::VertexRemoval, + source, + }, + ) if matches!( + source.as_ref(), + DelaunayRepairError::NonConvergent { max_flips: 0, .. } + ) => {} + InvariantError::Tds(TdsError::FacetSharingViolation { .. }) => {} + other => panic!( + "expected vertex-removal RepairOperationFailed from forced repair path, got {other:?}" + ), + } + + assert_eq!(dt.number_of_vertices(), vertex_count_before); + assert_eq!(dt.number_of_simplices(), simplex_count_before); + assert_eq!( + dt.insertion_state.last_inserted_simplex, last_inserted_simplex_before, + "remove_vertex rollback should restore last_inserted_simplex" + ); + assert_eq!( + dt.spatial_index + .as_ref() + .map(HashGridIndex::::debug_snapshot), + spatial_index_before, + "remove_vertex rollback should restore spatial_index" + ); + assert!(dt.vertices().any(|(_, v)| v.uuid() == inserted_uuid)); + assert!(dt.as_triangulation().validate().is_ok()); + } + + fn assert_remove_vertex_rollback() { + init_tracing(); + let vertices = simplex_vertices::(); + + let mut dt: DelaunayTriangulation, (), (), D> = + DelaunayTriangulation::new(&vertices).unwrap(); + dt.set_topology_guarantee(TopologyGuarantee::PLManifold); + + let simplex_key = dt.simplices().next().unwrap().0; + let inserted_vertex = interior_vertex_for_k1_insert::(); + let inserted_uuid = inserted_vertex.uuid(); + dt.flip_k1_insert(simplex_key, inserted_vertex).unwrap(); + + let vertex_key = dt + .vertices() + .find(|(_, v)| v.uuid() == inserted_uuid) + .map(|(k, _)| k) + .expect("Inserted vertex not found"); + + assert_forced_remove_vertex_rolls_back(&mut dt, vertex_key, inserted_uuid); + } + + fn assert_remove_vertex_fallback_rollback() { + init_tracing(); + let vertices = simplex_vertices::(); + + let mut dt: DelaunayTriangulation, (), (), D> = + DelaunayTriangulation::new(&vertices).unwrap(); + dt.set_topology_guarantee(TopologyGuarantee::PLManifold); + + let mut inserted_vertices = Vec::new(); + for point_index in 0..(D + 3) { + let inserted_vertex = rollback_probe_vertex::(point_index); + let inserted_uuid = inserted_vertex.uuid(); + let vertex_key = dt + .insert(inserted_vertex) + .expect("rollback fallback fixture insertion should succeed"); + inserted_vertices.push((vertex_key, inserted_uuid)); + } + + let (vertex_key, inserted_uuid, incident_simplices) = inserted_vertices + .iter() + .find_map(|&(vertex_key, inserted_uuid)| { + let incident_simplices = incident_simplex_count(&dt, vertex_key); + (incident_simplices != D + 1).then_some(( + vertex_key, + inserted_uuid, + incident_simplices, + )) + }) + .expect("expected at least one inserted vertex with a non-simplex star"); + assert_ne!( + incident_simplices, + D + 1, + "fallback rollback fixture must avoid the inverse-k=1 simplex-star path" + ); + + assert_forced_remove_vertex_rolls_back(&mut dt, vertex_key, inserted_uuid); + } + + macro_rules! gen_remove_vertex_rollback_tests { + ($dim:literal) => { + pastey::paste! { + #[test] + fn []() { + assert_remove_vertex_rollback::<$dim>(); + } + + #[test] + fn []() { + assert_remove_vertex_fallback_rollback::<$dim>(); + } + } + }; + } + + gen_remove_vertex_rollback_tests!(2); + gen_remove_vertex_rollback_tests!(3); + gen_remove_vertex_rollback_tests!(4); + gen_remove_vertex_rollback_tests!(5); + + #[test] + fn test_ridge_link_repair_validation_error_routes_tds_errors_to_tds_layer() { + let tds_err = TdsError::InvalidNeighbors { + reason: NeighborValidationError::Other { + message: "unit test".to_string(), + }, + }; + + match ridge_link_repair_validation_error(ManifoldError::Tds(tds_err.clone())) { + InsertionError::TopologyValidation(source) => assert_eq!(source, tds_err), + other => panic!("expected TopologyValidation, got {other:?}"), + } + } + + #[test] + fn test_ridge_link_repair_validation_error_routes_manifold_errors_to_triangulation_layer() { + let ridge_key = 0x1234_u64; + let error = ridge_link_repair_validation_error(ManifoldError::BoundaryRidgeMultiplicity { + ridge_key, + boundary_facet_count: 3, + }); + + match error { + InsertionError::TopologyValidationFailed { message, source } => { + assert_eq!(message, RIDGE_LINK_REPAIR_VALIDATION_MESSAGE); + assert!(matches!( + source, + TriangulationValidationError::BoundaryRidgeMultiplicity { + ridge_key: observed_ridge_key, + boundary_facet_count: 3 + } if observed_ridge_key == ridge_key + )); + } + other => panic!("expected TopologyValidationFailed, got {other:?}"), + } + } + + #[test] + fn test_remove_vertex_fast_path_inverse_k1() { + init_tracing(); + let vertices: Vec> = 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 mut dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + dt.set_topology_guarantee(TopologyGuarantee::PLManifold); + let original_vertex_count = dt.number_of_vertices(); + let original_simplex_count = dt.number_of_simplices(); + + let simplex_key = dt.simplices().next().unwrap().0; + let inserted_vertex = vertex!([0.2, 0.2, 0.2]); + let inserted_uuid = inserted_vertex.uuid(); + dt.flip_k1_insert(simplex_key, inserted_vertex).unwrap(); + + assert_eq!(dt.number_of_vertices(), original_vertex_count + 1); + assert_eq!(dt.number_of_simplices(), original_simplex_count + 3); + + let vertex_key = dt + .vertices() + .find(|(_, v)| v.uuid() == inserted_uuid) + .map(|(k, _)| k) + .expect("Inserted vertex not found"); + + dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); + let removed_simplices = dt.remove_vertex(vertex_key).unwrap(); + + assert_eq!(removed_simplices, 4); + assert_eq!(dt.number_of_vertices(), original_vertex_count); + assert_eq!(dt.number_of_simplices(), original_simplex_count); + assert!(dt.as_triangulation().validate().is_ok()); + assert!(dt.vertices().all(|(_, v)| v.uuid() != inserted_uuid)); + } + + #[test] + fn remove_vertex_invalidates_locate_hint_and_prunes_spatial_index() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + let vertex_key = dt.insert(vertex!([0.25, 0.25])).unwrap(); + let hint_simplex = dt.simplices().next().map(|(key, _)| key); + dt.insertion_state.last_inserted_simplex = hint_simplex; + let mut spatial_index = HashGridIndex::::new(1.0); + for (vertex_key, vertex) in dt.vertices() { + spatial_index.insert_vertex(vertex_key, vertex.point().coords()); + } + dt.spatial_index = Some(spatial_index); + assert!(dt.insertion_state.last_inserted_simplex.is_some()); + assert!(dt.spatial_index.is_some()); + + dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); + let removed_simplices = dt.remove_vertex(vertex_key).unwrap(); + + assert!(removed_simplices > 0); + assert!(dt.insertion_state.last_inserted_simplex.is_none()); + let spatial_index = dt + .spatial_index + .as_ref() + .expect("successful vertex removal should retain the spatial index"); + let mut found_removed_key = false; + assert!( + spatial_index.for_each_candidate_vertex_key(&[0.25, 0.25], |candidate| { + found_removed_key |= candidate == vertex_key; + true + }) + ); + assert!(!found_removed_key); + assert!(dt.as_triangulation().validate().is_ok()); + } + + #[test] + fn test_insert_single_interior_point_2d() { + init_tracing(); + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + assert_eq!(dt.number_of_vertices(), 3); + assert_eq!(dt.number_of_simplices(), 1); + + let v_key = dt.insert(vertex!([0.3, 0.3])).unwrap(); + + // Verify insertion succeeded + assert_eq!(dt.number_of_vertices(), 4); + assert_eq!(dt.number_of_simplices(), 3); + + // Verify the returned key can access the vertex + assert!(dt.tri.tds.vertex(v_key).is_some()); + } + + #[test] + fn test_insert_multiple_sequential_points_2d() { + init_tracing(); + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Insert 3 interior points sequentially + dt.insert(vertex!([0.3, 0.3])).unwrap(); + assert_eq!(dt.number_of_vertices(), 4); + + dt.insert(vertex!([0.5, 0.2])).unwrap(); + assert_eq!(dt.number_of_vertices(), 5); + + dt.insert(vertex!([0.2, 0.5])).unwrap(); + assert_eq!(dt.number_of_vertices(), 6); + + // All vertices should be present + assert!(dt.number_of_simplices() > 1); + } + + #[test] + fn test_insert_multiple_sequential_points_3d() { + init_tracing(); + 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 mut dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Insert 3 interior points sequentially (well inside the tetrahedron) + dt.insert(vertex!([0.1, 0.1, 0.1])).unwrap(); + assert_eq!(dt.number_of_vertices(), 5); + + dt.insert(vertex!([0.15, 0.15, 0.1])).unwrap(); + assert_eq!(dt.number_of_vertices(), 6); + + dt.insert(vertex!([0.1, 0.15, 0.15])).unwrap(); + assert_eq!(dt.number_of_vertices(), 7); + + assert!(dt.number_of_simplices() > 1); + } + + #[test] + fn test_insert_updates_last_inserted_simplex() { + init_tracing(); + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); + + // Initially no last_inserted_simplex + assert!(dt.insertion_state.last_inserted_simplex.is_none()); + + // After insertion, should have a cached simplex + dt.insert(vertex!([0.3, 0.3])).unwrap(); + assert!(dt.insertion_state.last_inserted_simplex.is_some()); + } + + #[test] + fn test_bootstrap_with_custom_kernel() { + init_tracing(); + // Verify bootstrap works with RobustKernel + let mut dt: DelaunayTriangulation, (), (), 3> = + DelaunayTriangulation::with_empty_kernel(RobustKernel::new()); + + assert_eq!(dt.number_of_vertices(), 0); + + // Bootstrap with robust predicates + dt.insert(vertex!([0.0, 0.0, 0.0])).unwrap(); + dt.insert(vertex!([1.0, 0.0, 0.0])).unwrap(); + dt.insert(vertex!([0.0, 1.0, 0.0])).unwrap(); + assert_eq!(dt.number_of_simplices(), 0); // Still bootstrapping + + dt.insert(vertex!([0.0, 0.0, 1.0])).unwrap(); + assert_eq!(dt.number_of_simplices(), 1); // Initial simplex created + + assert!(dt.is_valid().is_ok()); + } + + /// When the primary per-insertion repair returns `NonConvergent`, the robust + /// fallback in `maybe_repair_after_insertion` should rescue the insertion. + #[test] + fn test_maybe_repair_after_insertion_robust_fallback_on_forced_nonconvergent() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + let _guard = ForceRepairNonconvergentGuard::enable(); + let result = dt.insert(vertex!([0.5, 0.5])); + let inserted_key = result + .as_ref() + .copied() + .expect("Insertion should succeed via robust fallback"); + assert!( + result.is_ok(), + "Insertion should succeed via robust fallback: {result:?}" + ); + let spatial_index = dt + .spatial_index + .as_ref() + .expect("topology-only repair should preserve the duplicate-detection index"); + let mut found_inserted_key = false; + assert!( + spatial_index.for_each_candidate_vertex_key(&[0.5, 0.5], |candidate| { + found_inserted_key |= candidate == inserted_key; + true + }) + ); + assert!(found_inserted_key); + assert!(dt.validate().is_ok()); + } + + fn wedge_two_spheres_share_vertex_tds_2d() -> (Tds, SimplexKey, SimplexKey) { + // Two closed 2D spheres (boundaries of tetrahedra) sharing one vertex are + // pseudomanifold but not PL-manifold: the shared vertex has a disconnected link. + let mut tds: Tds = Tds::empty(); + + let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); + let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); + let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap(); + + let incident = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v3], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v2, v3], None).unwrap()) + .unwrap(); + let nonincident = tds + .insert_simplex_with_mapping(Simplex::new(vec![v1, v2, v3], None).unwrap()) + .unwrap(); + + let v4 = tds + .insert_vertex_with_mapping(vertex!([10.0, 10.0])) + .unwrap(); + let v5 = tds + .insert_vertex_with_mapping(vertex!([11.0, 10.0])) + .unwrap(); + let v6 = tds + .insert_vertex_with_mapping(vertex!([10.0, 11.0])) + .unwrap(); + + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v4, v5], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v4, v6], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v0, v5, v6], None).unwrap()) + .unwrap(); + let _ = tds + .insert_simplex_with_mapping(Simplex::new(vec![v4, v5, v6], None).unwrap()) + .unwrap(); + + (tds, incident, nonincident) + } + + #[test] + fn test_collect_local_repair_seed_simplices_merges_adjacent_extra_and_ignores_stale() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([1.0, 1.0]), + vertex!([0.5, 0.5]), + ]; + let dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + let all_simplices: Vec = + dt.simplices().map(|(simplex_key, _)| simplex_key).collect(); + + let (vertex_key, adjacent, extra_simplex) = dt + .vertices() + .find_map(|(vertex_key, _)| { + let adjacent: Vec = dt.tri.adjacent_simplices(vertex_key).collect(); + all_simplices + .iter() + .copied() + .find(|simplex_key| !adjacent.contains(simplex_key)) + .map(|extra_simplex| (vertex_key, adjacent, extra_simplex)) + }) + .expect("fixture should contain a simplex outside at least one vertex star"); + + let stale_simplex = SimplexKey::from(KeyData::from_ffi(999_999)); + let seeds = dt.collect_local_repair_seed_simplices( + vertex_key, + &[adjacent[0], extra_simplex, extra_simplex, stale_simplex], + ); + + assert_eq!(seeds.len(), adjacent.len() + 1); + assert_eq!(&seeds[..adjacent.len()], adjacent.as_slice()); + assert_eq!(seeds[adjacent.len()], extra_simplex); + assert!(!seeds.contains(&stale_simplex)); + } + + #[test] + fn test_validate_ridge_links_after_full_reseed_repair_uses_mutation_frontier() { + init_tracing(); + let (tds, incident_to_invalid_ridge, nonincident) = wedge_two_spheres_share_vertex_tds_2d(); + let dt = DelaunayTriangulation::from_tds_with_topology_guarantee( + tds, + AdaptiveKernel::new(), + TopologyGuarantee::PLManifold, + ); + let stats = DelaunayRepairStats { + flips_performed: 1, + ..DelaunayRepairStats::default() + }; + + let local_run = DelaunayRepairRun { + stats: stats.clone(), + touched_simplices: std::iter::once(nonincident).collect(), + used_full_reseed: true, + }; + assert!( + dt.validate_ridge_links_after_repair(TopologyGuarantee::PLManifold, &local_run) + .is_ok() + ); + + let invalid_scope_run = DelaunayRepairRun { + stats, + touched_simplices: std::iter::once(incident_to_invalid_ridge).collect(), + used_full_reseed: true, + }; + assert!( + dt.validate_ridge_links_after_repair( + TopologyGuarantee::PLManifold, + &invalid_scope_run, + ) + .is_err() + ); + } +} diff --git a/src/triangulation/locality.rs b/src/delaunay/locality.rs similarity index 98% rename from src/triangulation/locality.rs rename to src/delaunay/locality.rs index 1e15d6a7..37c11f34 100644 --- a/src/triangulation/locality.rs +++ b/src/delaunay/locality.rs @@ -26,7 +26,7 @@ pub struct LocalConflictSeedSimplices { /// Adds live, deduplicated candidate simplices to a pending local repair frontier. /// /// Returns the number of simplices newly appended to `pending_seed_simplices`. -pub(super) fn accumulate_live_simplex_seeds( +pub fn accumulate_live_simplex_seeds( tds: &Tds, candidate_seed_simplices: &[SimplexKey], pending_seed_simplices: &mut Vec, @@ -76,7 +76,7 @@ where } /// Retains only live, deduplicated simplices in a pending local repair frontier. -pub(super) fn retain_live_simplex_seeds( +pub fn retain_live_simplex_seeds( tds: &Tds, seed_simplices: &mut Vec, seen: &mut FastHashSet, @@ -90,7 +90,7 @@ pub(super) fn retain_live_simplex_seeds( } /// Clears a local repair frontier and its deduplication set together. -pub(super) fn clear_simplex_seed_set( +pub fn clear_simplex_seed_set( seed_simplices: &mut Vec, seen: &mut FastHashSet, ) { @@ -174,7 +174,7 @@ mod tests { use crate::geometry::kernel::FastKernel; use crate::geometry::point::Point; use crate::geometry::traits::coordinate::Coordinate; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use slotmap::KeyData; diff --git a/src/delaunay/query.rs b/src/delaunay/query.rs new file mode 100644 index 00000000..14846440 --- /dev/null +++ b/src/delaunay/query.rs @@ -0,0 +1,1308 @@ +//! Read-only Delaunay triangulation query, traversal, and accessor methods. +//! +//! This module owns the high-level forwarding surface for inspecting a +//! `DelaunayTriangulation`: counts, iterator access, TDS views, topology +//! metadata accessors, and adjacency traversal helpers. It also keeps the small +//! cache invalidation helpers next to the accessors they protect. + +#![forbid(unsafe_code)] + +use crate::core::adjacency::{AdjacencyIndex, AdjacencyIndexBuildError}; +use crate::core::edge::EdgeKey; +use crate::core::facet::{AllFacetsIter, BoundaryFacetsIter}; +use crate::core::simplex::Simplex; +use crate::core::tds::{SimplexKey, Tds, VertexKey}; +use crate::core::traits::data_type::DataType; +use crate::core::triangulation::Triangulation; +use crate::core::validation::{TopologyGuarantee, ValidationPolicy}; +use crate::core::vertex::Vertex; +use crate::geometry::kernel::Kernel; +use crate::repair::{DelaunayCheckPolicy, DelaunayRepairPolicy}; +use crate::topology::traits::topological_space::{GlobalTopology, TopologyKind}; +use crate::triangulation::DelaunayTriangulation; + +// ============================================================================= +// QUERY, ACCESSORS, AND CONFIGURATION (Minimal Bounds) +// ============================================================================= +// +// Methods that only need `K: Kernel` — no scalar arithmetic. Downstream +// generic code (e.g. `delaunayize_by_flips`) does not need to carry +// `CoordinateScalar + NumCast` bounds when calling these methods. +// +// Follows the precedent of the existing PURE STRUCT ASSEMBLY impl block. + +impl DelaunayTriangulation +where + K: Kernel, +{ + // ------------------------------------------------------------------------- + // QUERY / ACCESSORS + // ------------------------------------------------------------------------- + + /// Returns the number of vertices in the triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0, 0.0, 0.0]), + /// vertex!([1.0, 0.0, 0.0, 0.0]), + /// vertex!([0.0, 1.0, 0.0, 0.0]), + /// vertex!([0.0, 0.0, 1.0, 0.0]), + /// vertex!([0.0, 0.0, 0.0, 1.0]), + /// vertex!([0.2, 0.2, 0.2, 0.2]), + /// ]; + /// + /// let dt: DelaunayTriangulation<_, (), (), 4> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// assert_eq!(dt.number_of_vertices(), 6); + /// ``` + #[must_use] + pub fn number_of_vertices(&self) -> usize { + self.tri.number_of_vertices() + } + + /// Returns the number of simplices in the triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0, 0.0, 0.0]), + /// vertex!([1.0, 0.0, 0.0, 0.0]), + /// vertex!([0.0, 1.0, 0.0, 0.0]), + /// vertex!([0.0, 0.0, 1.0, 0.0]), + /// vertex!([0.0, 0.0, 0.0, 1.0]), + /// ]; + /// + /// let dt: DelaunayTriangulation<_, (), (), 4> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// // One 4-simplex in 4D + /// assert_eq!(dt.number_of_simplices(), 1); + /// ``` + #[must_use] + pub fn number_of_simplices(&self) -> usize { + self.tri.number_of_simplices() + } + + /// Returns the dimension of the triangulation. + /// + /// Returns the dimension `D` as an `i32`. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0, 0.0, 0.0]), + /// vertex!([1.0, 0.0, 0.0, 0.0]), + /// vertex!([0.0, 1.0, 0.0, 0.0]), + /// vertex!([0.0, 0.0, 1.0, 0.0]), + /// vertex!([0.0, 0.0, 0.0, 1.0]), + /// ]; + /// + /// let dt: DelaunayTriangulation<_, (), (), 4> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// assert_eq!(dt.dim(), 4); + /// ``` + #[must_use] + pub fn dim(&self) -> i32 { + self.tri.dim() + } + + /// 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::query::*; + /// + /// 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::new(&vertices).unwrap(); + /// + /// for (simplex_key, simplex) in dt.simplices() { + /// println!("Simplex {:?} has {} vertices", simplex_key, simplex.number_of_vertices()); + /// } + /// ``` + pub fn simplices(&self) -> impl Iterator)> { + self.tri.tds.simplices() + } + + /// 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::query::*; + /// + /// 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::new(&vertices).unwrap(); + /// + /// for (vertex_key, vertex) in dt.vertices() { + /// println!("Vertex {:?} at {:?}", vertex_key, vertex.point()); + /// } + /// ``` + pub fn vertices(&self) -> impl Iterator)> { + self.tri.vertices() + } + + /// 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 + /// + /// `None` if the key is not found. `Some(previous)` where `previous` is + /// the old `Option` value if the key exists. + /// + /// # Examples + /// + /// ``` + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulationBuilder, Vertex, vertex, + /// }; + /// + /// let vertices: [Vertex; 3] = [ + /// vertex!([0.0, 0.0], 10i32), + /// vertex!([1.0, 0.0], 20), + /// vertex!([0.0, 1.0], 30), + /// ]; + /// let mut dt = DelaunayTriangulationBuilder::new(&vertices) + /// .build::<()>() + /// .unwrap(); + /// let key = dt.vertices().next().unwrap().0; + /// + /// 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(Some(99))); + /// assert_eq!(dt.tds().vertex(key).unwrap().data(), None); + /// ``` + #[inline] + pub fn set_vertex_data(&mut self, key: VertexKey, data: Option) -> Option> { + 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 + /// + /// `None` if the key is not found. `Some(previous)` where `previous` is + /// the old `Option` value if the key exists. + /// + /// # Examples + /// + /// ``` + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulationBuilder, vertex, + /// }; + /// + /// let vertices = [ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let mut dt = DelaunayTriangulationBuilder::new(&vertices) + /// .build::() + /// .unwrap(); + /// let key = dt.simplices().next().unwrap().0; + /// + /// let prev = dt.set_simplex_data(key, Some(42)); + /// assert_eq!(prev, Some(None)); + /// + /// // Clear data + /// let prev = dt.set_simplex_data(key, None); + /// assert_eq!(prev, Some(Some(42))); + /// assert_eq!(dt.tds().simplex(key).unwrap().data(), None); + /// ``` + #[inline] + pub fn set_simplex_data(&mut self, key: SimplexKey, data: Option) -> Option> { + self.tri.tds.set_simplex_data(key, data) + } + + /// Returns a reference to the underlying triangulation data structure. + /// + /// This provides access to the purely combinatorial Tds layer for + /// advanced operations and performance testing. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0, 0.0, 0.0]), + /// vertex!([1.0, 0.0, 0.0, 0.0]), + /// vertex!([0.0, 1.0, 0.0, 0.0]), + /// vertex!([0.0, 0.0, 1.0, 0.0]), + /// vertex!([0.0, 0.0, 0.0, 1.0]), + /// ]; + /// + /// let dt: DelaunayTriangulation<_, (), (), 4> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// let tds = dt.tds(); + /// assert_eq!(tds.number_of_vertices(), 5); + /// ``` + #[must_use] + pub const fn tds(&self) -> &Tds { + &self.tri.tds + } + + /// Returns a mutable reference to the underlying triangulation data structure. + /// + /// This provides mutable access to the purely combinatorial Tds layer for + /// advanced operations and testing of internal algorithms. + /// + /// # Safety + /// + /// 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. + /// + /// 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::from_triangulation()`. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::ConvexHull; + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// let vertices: Vec<_> = 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::new(&vertices).unwrap(); + /// let hull = ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// assert_eq!(hull.number_of_facets(), 4); + /// ``` + #[must_use] + pub const fn as_triangulation(&self) -> &Triangulation { + &self.tri + } + + /// Returns the insertion-time global topology validation policy used by the underlying + /// triangulation. + /// + /// This policy controls when Level 3 (`Triangulation::is_valid()`) is run automatically + /// during incremental insertion (as part of the topology safety net). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// + /// let dt: DelaunayTriangulation<_, (), (), 2> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// assert_eq!( + /// dt.validation_policy(), + /// delaunay::prelude::validation::ValidationPolicy::OnSuspicion + /// ); + /// ``` + #[inline] + #[must_use] + pub const fn validation_policy(&self) -> ValidationPolicy { + self.tri.validation_policy + } + + /// Sets 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()`.) + /// + /// If the requested policy is incompatible with the current topology guarantee (for example, + /// `ValidationPolicy::Never` with `TopologyGuarantee::PLManifold`), this runs + /// [`Triangulation::validate_at_completion`](crate::Triangulation::validate_at_completion) + /// to provide immediate feedback and emits a warning. Call `validate_at_completion()` after + /// batch construction when using an incompatible combination. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::validation::ValidationPolicy; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// + /// let mut dt: DelaunayTriangulation<_, (), (), 2> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// 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); + } + /// Returns the automatic Delaunay repair policy. + #[inline] + #[must_use] + pub const fn delaunay_repair_policy(&self) -> DelaunayRepairPolicy { + self.insertion_state.delaunay_repair_policy + } + + /// Sets the automatic Delaunay repair policy. + #[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. + #[inline] + #[must_use] + pub const fn delaunay_check_policy(&self) -> DelaunayCheckPolicy { + self.insertion_state.delaunay_check_policy + } + + /// Sets the automatic global Delaunay validation policy. + #[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 +where + K: Kernel, + U: DataType, + V: DataType, +{ + // ------------------------------------------------------------------------- + // CONFIGURATION + // ------------------------------------------------------------------------- + + /// Returns the topology guarantee used for Level 3 topology validation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulationBuilder, TopologyGuarantee, vertex, + /// }; + /// + /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); + /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); + /// ``` + #[inline] + #[must_use] + pub const fn topology_guarantee(&self) -> TopologyGuarantee { + self.tri.topology_guarantee() + } + + /// Returns runtime global topology metadata associated with this triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulationBuilder, GlobalTopology, vertex, + /// }; + /// + /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); + /// assert!(dt.global_topology().is_euclidean()); + /// ``` + #[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::{ + /// DelaunayTriangulationBuilder, TopologyKind, vertex, + /// }; + /// + /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; + /// let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); + /// assert_eq!(dt.topology_kind(), TopologyKind::Euclidean); + /// ``` + #[inline] + #[must_use] + pub const fn topology_kind(&self) -> TopologyKind { + self.tri.topology_kind() + } + + /// Sets runtime global topology metadata on this triangulation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulationBuilder, GlobalTopology, vertex, + /// }; + /// + /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; + /// let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); + /// dt.set_global_topology(GlobalTopology::Euclidean); + /// assert!(dt.global_topology().is_euclidean()); + /// ``` + #[inline] + pub const fn set_global_topology(&mut self, global_topology: GlobalTopology) { + self.tri.set_global_topology(global_topology); + } + + /// Sets the topology guarantee used for Level 3 topology validation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulation, TopologyGuarantee, + /// }; + /// + /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::empty(); + /// dt.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + /// + /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); + /// ``` + #[inline] + pub fn set_topology_guarantee(&mut self, guarantee: TopologyGuarantee) { + self.tri.set_topology_guarantee(guarantee); + } + + /// 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 + /// + /// An iterator yielding `FacetView` objects for all facets. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// 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::new(&vertices).unwrap(); + /// + /// let facet_count = dt.facets().count(); + /// assert_eq!(facet_count, 4); // Tetrahedron has 4 facets + /// ``` + pub fn facets(&self) -> AllFacetsIter<'_, K::Scalar, U, V, D> { + self.tri.facets() + } + + /// Returns an iterator over boundary (hull) facets in the triangulation. + /// + /// Boundary facets are those that belong to exactly one simplex. This method + /// computes the facet-to-simplices map internally for convenience. + /// + /// # Returns + /// + /// An iterator yielding `FacetView` objects for boundary facets only. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// 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::new(&vertices).unwrap(); + /// + /// let boundary_count = dt.boundary_facets().count(); + /// assert_eq!(boundary_count, 4); // All facets are on boundary + /// ``` + pub fn boundary_facets(&self) -> BoundaryFacetsIter<'_, K::Scalar, U, V, D> { + self.tri.boundary_facets() + } + + /// Builds an immutable adjacency index for fast repeated topology queries. + /// + /// This is a convenience wrapper around + /// [`Triangulation::build_adjacency_index`](crate::Triangulation::build_adjacency_index). + /// + /// # Errors + /// + /// Returns an error if the underlying triangulation data structure is internally inconsistent + /// (e.g., a simplex references a missing vertex key or a missing neighbor simplex key). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// // A single 3D tetrahedron has 6 unique edges. + /// 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::new(&vertices).unwrap(); + /// let index = dt.build_adjacency_index().unwrap(); + /// + /// assert_eq!(index.number_of_edges(), 6); + /// ``` + #[inline] + pub fn build_adjacency_index(&self) -> Result { + self.as_triangulation().build_adjacency_index() + } + + /// Returns an iterator over all unique edges in the triangulation. + /// + /// This is a convenience wrapper around + /// [`Triangulation::edges`](crate::Triangulation::edges). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// // A single 3D tetrahedron has 6 unique edges. + /// 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::new(&vertices).unwrap(); + /// let edges: std::collections::HashSet<_> = dt.edges().collect(); + /// assert_eq!(edges.len(), 6); + /// ``` + pub fn edges(&self) -> impl Iterator + '_ { + self.as_triangulation().edges() + } + + /// Returns an iterator over all unique edges using a precomputed [`AdjacencyIndex`]. + /// + /// This avoids per-call deduplication and allocations. + /// + /// This is a convenience wrapper around + /// [`Triangulation::edges_with_index`](crate::Triangulation::edges_with_index). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// 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::new(&vertices).unwrap(); + /// let index = dt.build_adjacency_index().unwrap(); + /// + /// let edges: std::collections::HashSet<_> = dt.edges_with_index(&index).collect(); + /// assert_eq!(edges.len(), 6); + /// ``` + pub fn edges_with_index<'a>( + &self, + index: &'a AdjacencyIndex, + ) -> impl Iterator + 'a { + self.as_triangulation().edges_with_index(index) + } + + /// Returns an iterator over all unique edges incident to a vertex. + /// + /// This is a convenience wrapper around + /// [`Triangulation::incident_edges`](crate::Triangulation::incident_edges). + /// + /// If `v` is not present in this triangulation, the iterator is empty. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// 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::new(&vertices).unwrap(); + /// let v0 = dt.vertices().next().unwrap().0; + /// + /// // In a tetrahedron, each vertex has degree 3. + /// assert_eq!(dt.incident_edges(v0).count(), 3); + /// ``` + pub fn incident_edges(&self, v: VertexKey) -> impl Iterator + '_ { + self.as_triangulation().incident_edges(v) + } + + /// Returns an iterator over all unique edges incident to a vertex using a precomputed + /// [`AdjacencyIndex`]. + /// + /// If `v` is not present in the index, the iterator is empty. + /// + /// This is a convenience wrapper around + /// [`Triangulation::incident_edges_with_index`](crate::Triangulation::incident_edges_with_index). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// 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::new(&vertices).unwrap(); + /// let index = dt.build_adjacency_index().unwrap(); + /// let v0 = dt.vertices().next().unwrap().0; + /// + /// assert_eq!(dt.incident_edges_with_index(&index, v0).count(), 3); + /// ``` + pub fn incident_edges_with_index<'a>( + &self, + index: &'a AdjacencyIndex, + v: VertexKey, + ) -> impl Iterator + 'a { + self.as_triangulation().incident_edges_with_index(index, v) + } + + /// Returns an iterator over all neighbors of a simplex. + /// + /// Boundary facets are omitted (only existing neighbors are yielded). If `c` is not + /// present, the iterator is empty. + /// + /// This is a convenience wrapper around + /// [`Triangulation::simplex_neighbors`](crate::Triangulation::simplex_neighbors). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// // A single tetrahedron has no simplex neighbors. + /// 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::new(&vertices).unwrap(); + /// let simplex_key = dt.simplices().next().unwrap().0; + /// assert_eq!(dt.simplex_neighbors(simplex_key).count(), 0); + /// ``` + pub fn simplex_neighbors(&self, c: SimplexKey) -> impl Iterator + '_ { + self.as_triangulation().simplex_neighbors(c) + } + + /// Returns an iterator over all neighbors of a simplex using a precomputed [`AdjacencyIndex`]. + /// + /// If `c` is not present in the index, the iterator is empty. + /// + /// This is a convenience wrapper around + /// [`Triangulation::simplex_neighbors_with_index`](crate::Triangulation::simplex_neighbors_with_index). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// // Two tetrahedra sharing a triangular facet => each tetra has exactly one neighbor. + /// let vertices: Vec<_> = vec![ + /// // Shared triangle + /// vertex!([0.0, 0.0, 0.0]), + /// vertex!([2.0, 0.0, 0.0]), + /// vertex!([1.0, 2.0, 0.0]), + /// // Two apices + /// vertex!([1.0, 0.7, 1.5]), + /// vertex!([1.0, 0.7, -1.5]), + /// ]; + /// + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let index = dt.build_adjacency_index().unwrap(); + /// + /// let simplex_key = dt.simplices().next().unwrap().0; + /// assert_eq!(dt.simplex_neighbors_with_index(&index, simplex_key).count(), 1); + /// ``` + pub fn simplex_neighbors_with_index<'a>( + &self, + index: &'a AdjacencyIndex, + c: SimplexKey, + ) -> impl Iterator + 'a { + self.as_triangulation() + .simplex_neighbors_with_index(index, c) + } + + /// Returns a slice view of a simplex's vertex keys. + /// + /// This is a zero-allocation accessor. If `c` is not present, returns `None`. + /// + /// This is a convenience wrapper around + /// [`Triangulation::simplex_vertices`](crate::Triangulation::simplex_vertices). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// let simplex_key = dt.simplices().next().unwrap().0; + /// let simplex_vertices = dt.simplex_vertices(simplex_key).unwrap(); + /// assert_eq!(simplex_vertices.len(), 3); // D+1 for a 2D simplex + /// ``` + #[must_use] + pub fn simplex_vertices(&self, c: SimplexKey) -> Option<&[VertexKey]> { + self.as_triangulation().simplex_vertices(c) + } + + /// Returns a slice view of a vertex's coordinates. + /// + /// This is a zero-allocation accessor. If `v` is not present, returns `None`. + /// + /// This is a convenience wrapper around + /// [`Triangulation::vertex_coords`](crate::Triangulation::vertex_coords). + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// // Find the key for a known vertex by matching coordinates. + /// let v_key = dt + /// .vertices() + /// .find_map(|(vk, _)| (dt.vertex_coords(vk)? == [1.0, 0.0]).then_some(vk)) + /// .unwrap(); + /// + /// assert_eq!(dt.vertex_coords(v_key).unwrap(), [1.0, 0.0]); + /// ``` + #[must_use] + pub fn vertex_coords(&self, v: VertexKey) -> Option<&[K::Scalar]> { + self.as_triangulation().vertex_coords(v) + } +} +#[cfg(test)] +mod tests { + use super::*; + use crate::geometry::kernel::{AdaptiveKernel, FastKernel}; + use crate::vertex; + use std::{collections::HashSet, num::NonZeroUsize, sync::Once}; + + fn init_tracing() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_test_writer() + .try_init(); + }); + } + + #[test] + fn test_delaunay_constructors_default_to_pl_manifold_mode() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + + let dt_new: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + assert_eq!(dt_new.topology_guarantee(), TopologyGuarantee::PLManifold); + + let dt_empty: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); + assert_eq!(dt_empty.topology_guarantee(), TopologyGuarantee::PLManifold); + + let dt_with_kernel: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices).unwrap(); + + assert_eq!( + dt_with_kernel.topology_guarantee(), + TopologyGuarantee::PLManifold + ); + + let dt_from_tds: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::try_from_tds(dt_new.tds().clone(), FastKernel::new()).unwrap(); + assert_eq!( + dt_from_tds.topology_guarantee(), + TopologyGuarantee::PLManifold + ); + } + + #[test] + fn test_set_topology_guarantee_updates_underlying_triangulation() { + init_tracing(); + let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); + + assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); + assert_eq!(dt.tri.topology_guarantee, TopologyGuarantee::PLManifold); + + dt.set_topology_guarantee(TopologyGuarantee::Pseudomanifold); + assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); + assert_eq!(dt.tri.topology_guarantee, TopologyGuarantee::Pseudomanifold); + } + + #[test] + fn test_set_delaunay_check_policy_updates_state() { + init_tracing(); + let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); + assert_eq!(dt.delaunay_check_policy(), DelaunayCheckPolicy::EndOnly); + + let policy = DelaunayCheckPolicy::EveryN(NonZeroUsize::new(3).unwrap()); + dt.set_delaunay_check_policy(policy); + assert_eq!(dt.delaunay_check_policy(), policy); + } + + #[test] + fn test_validation_policy_defaults_to_on_suspicion() { + init_tracing(); + // empty() -> Triangulation::new_empty() -> ValidationPolicy::default() + let dt_empty: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); + assert_eq!(dt_empty.validation_policy(), ValidationPolicy::OnSuspicion); + + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + + // new() -> with_kernel() -> explicit validation_policy initialization + let dt_new: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + assert_eq!(dt_new.validation_policy(), ValidationPolicy::OnSuspicion); + + // with_kernel() constructor path should also use the default policy + let dt_with_kernel: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices).unwrap(); + assert_eq!( + dt_with_kernel.validation_policy(), + ValidationPolicy::OnSuspicion + ); + + // try_from_tds() is a separate reconstruction path and should also + // default to OnSuspicion after validation succeeds. + let tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let dt_from_tds: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::try_from_tds(tds, FastKernel::new()).unwrap(); + assert_eq!( + dt_from_tds.validation_policy(), + ValidationPolicy::OnSuspicion + ); + } + + #[test] + fn test_validation_policy_setter_and_getter_roundtrip() { + init_tracing(); + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Getter reflects the underlying Triangulation policy. + assert_eq!(dt.validation_policy(), ValidationPolicy::OnSuspicion); + assert_eq!(dt.tri.validation_policy, ValidationPolicy::OnSuspicion); + + dt.set_validation_policy(ValidationPolicy::Always); + assert_eq!(dt.validation_policy(), ValidationPolicy::Always); + assert_eq!(dt.tri.validation_policy, ValidationPolicy::Always); + + dt.set_validation_policy(ValidationPolicy::Never); + assert_eq!(dt.validation_policy(), ValidationPolicy::Never); + assert_eq!(dt.tri.validation_policy, ValidationPolicy::Never); + + dt.set_validation_policy(ValidationPolicy::OnSuspicion); + assert_eq!(dt.validation_policy(), ValidationPolicy::OnSuspicion); + assert_eq!(dt.tri.validation_policy, ValidationPolicy::OnSuspicion); + } + + #[test] + fn test_number_of_vertices_minimal_simplex() { + init_tracing(); + 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::new(&vertices).unwrap(); + + assert_eq!(dt.number_of_vertices(), 4); + } + + #[test] + fn test_number_of_simplices_minimal_simplex() { + init_tracing(); + 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::new(&vertices).unwrap(); + + // Minimal 3D simplex has exactly 1 tetrahedron + assert_eq!(dt.number_of_simplices(), 1); + } + + #[test] + fn test_number_of_simplices_after_insertion() { + init_tracing(); + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + assert_eq!(dt.number_of_simplices(), 1); + + // Insert interior point - should create 3 triangles + dt.insert(vertex!([0.3, 0.3])).unwrap(); + assert_eq!(dt.number_of_simplices(), 3); + } + + #[test] + fn test_dim_returns_correct_dimension() { + init_tracing(); + let vertices_2d = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let dt_2d: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices_2d).unwrap(); + assert_eq!(dt_2d.dim(), 2); + + let vertices_3d = 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_3d: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices_3d).unwrap(); + assert_eq!(dt_3d.dim(), 3); + + let vertices_4d = vec![ + vertex!([0.0, 0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0, 0.0]), + vertex!([0.0, 1.0, 0.0, 0.0]), + vertex!([0.0, 0.0, 1.0, 0.0]), + vertex!([0.0, 0.0, 0.0, 1.0]), + ]; + let dt_4d: DelaunayTriangulation<_, (), (), 4> = + DelaunayTriangulation::new(&vertices_4d).unwrap(); + assert_eq!(dt_4d.dim(), 4); + } + + #[test] + fn test_new_with_exact_minimum_vertices() { + init_tracing(); + // 2D: exactly 3 vertices (minimum for 2D simplex) + let vertices_2d = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let dt_2d: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices_2d).unwrap(); + assert_eq!(dt_2d.number_of_vertices(), 3); + assert_eq!(dt_2d.number_of_simplices(), 1); + + // 3D: exactly 4 vertices (minimum for 3D simplex) + let vertices_3d = 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_3d: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices_3d).unwrap(); + assert_eq!(dt_3d.number_of_vertices(), 4); + assert_eq!(dt_3d.number_of_simplices(), 1); + } + + #[test] + fn test_tds_accessor_provides_readonly_access() { + init_tracing(); + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Access TDS via immutable reference + let tds = dt.tds(); + assert_eq!(tds.number_of_vertices(), 3); + assert_eq!(tds.number_of_simplices(), 1); + + // Verify we can call other TDS methods + assert!(tds.is_valid().is_ok()); + assert!(tds.simplex_keys().next().is_some()); + } + + #[test] + fn test_internal_tds_access() { + init_tracing(); + 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 mut dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + + assert_eq!(dt.number_of_vertices(), 4); + + // Internal code can access TDS directly for mutations + let tds = &mut dt.tri.tds; + assert_eq!(tds.number_of_vertices(), 4); + assert_eq!(tds.number_of_simplices(), 1); + + // Can call mutating methods like remove_duplicate_simplices + let result = tds.remove_duplicate_simplices(); + assert!(result.is_ok()); + } + + #[test] + fn test_tds_accessor_reflects_insertions() { + init_tracing(); + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Before insertion + assert_eq!(dt.tds().number_of_vertices(), 3); + + // Insert a new vertex + dt.insert(vertex!([0.3, 0.3])).unwrap(); + + // After insertion, TDS accessor reflects the change + assert_eq!(dt.tds().number_of_vertices(), 4); + assert!(dt.tds().number_of_simplices() > 1); + } + + #[test] + fn test_tds_accessors_maintain_validation_invariants() { + init_tracing(); + let vertices = vec![ + vertex!([0.0, 0.0, 0.0, 0.0]), + vertex!([1.0, 0.0, 0.0, 0.0]), + vertex!([0.0, 1.0, 0.0, 0.0]), + vertex!([0.0, 0.0, 1.0, 0.0]), + vertex!([0.0, 0.0, 0.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 4> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Verify TDS is valid through accessor + assert!(dt.tds().is_valid().is_ok()); + + // Insert additional vertex + dt.insert(vertex!([0.2, 0.2, 0.2, 0.2])).unwrap(); + + // TDS should still be valid after mutation + assert!(dt.tds().is_valid().is_ok()); + assert!(dt.tds().validate().is_ok()); + } + + #[test] + fn test_topology_traversal_methods_are_forwarded() { + init_tracing(); + // Single tetrahedron: 4 vertices, 1 simplex, 6 unique edges. + 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::new(&vertices).unwrap(); + let tri = dt.as_triangulation(); + + let edges_dt: HashSet<_> = dt.edges().collect(); + let edges_tri: HashSet<_> = tri.edges().collect(); + assert_eq!(edges_dt, edges_tri); + assert_eq!(edges_dt.len(), 6); + + let index = dt.build_adjacency_index().unwrap(); + let edges_dt_index: HashSet<_> = dt.edges_with_index(&index).collect(); + let edges_tri_index: HashSet<_> = tri.edges_with_index(&index).collect(); + assert_eq!(edges_dt_index, edges_tri_index); + assert_eq!(edges_dt_index, edges_dt); + + let v0 = dt.vertices().next().unwrap().0; + let incident_dt: HashSet<_> = dt.incident_edges(v0).collect(); + let incident_tri: HashSet<_> = tri.incident_edges(v0).collect(); + assert_eq!(incident_dt, incident_tri); + assert_eq!(incident_dt.len(), 3); + + let incident_dt_index: HashSet<_> = dt.incident_edges_with_index(&index, v0).collect(); + let incident_tri_index: HashSet<_> = tri.incident_edges_with_index(&index, v0).collect(); + assert_eq!(incident_dt_index, incident_tri_index); + assert_eq!(incident_dt_index, incident_dt); + + let simplex_key = dt.simplices().next().unwrap().0; + let neighbors_dt: Vec<_> = dt.simplex_neighbors(simplex_key).collect(); + let neighbors_tri: Vec<_> = tri.simplex_neighbors(simplex_key).collect(); + assert_eq!(neighbors_dt, neighbors_tri); + assert!(neighbors_dt.is_empty()); + + let neighbors_dt_index: Vec<_> = dt + .simplex_neighbors_with_index(&index, simplex_key) + .collect(); + let neighbors_tri_index: Vec<_> = tri + .simplex_neighbors_with_index(&index, simplex_key) + .collect(); + assert_eq!(neighbors_dt_index, neighbors_tri_index); + assert_eq!(neighbors_dt_index, neighbors_dt); + + // Geometry/topology accessors should be forwarded as well. + let simplex_vertices_dt = dt.simplex_vertices(simplex_key).unwrap(); + let simplex_vertices_tri = tri.simplex_vertices(simplex_key).unwrap(); + assert_eq!(simplex_vertices_dt, simplex_vertices_tri); + assert_eq!(simplex_vertices_dt.len(), 4); + + let coords_dt = dt.vertex_coords(v0).unwrap(); + let coords_tri = tri.vertex_coords(v0).unwrap(); + assert_eq!(coords_dt, coords_tri); + + // Missing keys should behave the same as on `Triangulation`. + assert!(dt.vertex_coords(VertexKey::default()).is_none()); + assert!(dt.simplex_vertices(SimplexKey::default()).is_none()); + } +} diff --git a/src/delaunay/repair.rs b/src/delaunay/repair.rs new file mode 100644 index 00000000..0a24fba2 --- /dev/null +++ b/src/delaunay/repair.rs @@ -0,0 +1,1468 @@ +//! Repair policies and outcomes for Delaunay triangulations. +//! +//! This module separates mutating Delaunay repair policy from validation-only +//! checking. [`DelaunayRepairPolicy`] controls when construction and editing +//! paths may run local flip repair, while [`DelaunayCheckPolicy`] controls +//! global Level 4 validation cadence without mutating topology. +//! +//! Import these APIs through [`delaunay::prelude::repair`](crate::prelude::repair) +//! for downstream examples, tests, and applications. + +#![forbid(unsafe_code)] + +#[cfg(test)] +use crate::construction::test_hooks; +use crate::core::algorithms::flips::{ + DelaunayRepairError, DelaunayRepairRun, DelaunayRepairStats, repair_delaunay_with_flips_k2_k3, + repair_delaunay_with_flips_k2_k3_run, +}; +use crate::core::collections::FastHasher; +use crate::core::operations::{InsertionOutcome, RepairDecision, TopologicalOperation}; +use crate::core::tds::SimplexKey; +use crate::core::traits::data_type::DataType; +use crate::core::util::stable_hash_u64_slice; +use crate::core::validation::TopologyGuarantee; +use crate::core::vertex::Vertex; +use crate::geometry::kernel::{ExactPredicates, Kernel, RobustKernel}; +use crate::triangulation::DelaunayTriangulation; +use num_traits::NumCast; +use rand::SeedableRng; +use rand::seq::SliceRandom; +use std::{ + fmt, + hash::{Hash, Hasher}, + num::NonZeroUsize, +}; + +// Heuristic rebuild attempts must be consistent across build profiles to avoid +// release-only construction failures (see #306). +const HEURISTIC_REBUILD_ATTEMPTS: usize = 6; + +thread_local! { + static HEURISTIC_REBUILD_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +struct HeuristicRebuildRecursionGuard { + prior_depth: usize, +} + +impl HeuristicRebuildRecursionGuard { + /// Tracks nested heuristic rebuilds so fallback construction cannot recurse + /// indefinitely through repair hooks. + fn enter() -> Self { + let prior_depth = HEURISTIC_REBUILD_DEPTH.with(|depth| { + let prior = depth.get(); + depth.set(prior.saturating_add(1)); + prior + }); + Self { prior_depth } + } +} + +impl Drop for HeuristicRebuildRecursionGuard { + fn drop(&mut self) { + HEURISTIC_REBUILD_DEPTH.with(|depth| depth.set(self.prior_depth)); + } +} + +/// Mutating Delaunay operation that can invoke flip-based repair internally. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::repair::DelaunayRepairOperation; +/// +/// assert_eq!(DelaunayRepairOperation::VertexRemoval.to_string(), "vertex removal"); +/// ``` +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum DelaunayRepairOperation { + /// Repair after removing a vertex. + VertexRemoval, +} + +impl fmt::Display for DelaunayRepairOperation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::VertexRemoval => f.write_str("vertex removal"), + } + } +} + +/// Policy controlling automatic flip-based Delaunay repair. +/// +/// This policy schedules **local flip-based repairs** after successful insertions +/// (and removals that modify topology). +/// It is separate from any *validation-only* policy to allow checking the Delaunay +/// property without mutating topology when needed. +/// +/// During batch construction, [`DelaunayRepairPolicy::EveryN`] is a scheduled +/// cadence rather than a hard lower bound on repair frequency: construction may +/// run an additional local repair earlier when the accumulated seed frontier +/// grows large. [`DelaunayRepairPolicy::Never`] disables those automatic batch +/// repairs. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::repair::DelaunayRepairPolicy; +/// use std::num::NonZeroUsize; +/// +/// # fn main() -> Result<(), Box> { +/// let every_four = NonZeroUsize::new(4).ok_or("repair cadence must be non-zero")?; +/// let policy = DelaunayRepairPolicy::EveryN(every_four); +/// assert!(!policy.should_repair(0)); +/// assert!(!policy.should_repair(3)); +/// assert!(policy.should_repair(4)); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DelaunayRepairPolicy { + /// Disable automatic Delaunay repairs. + Never, + /// Run local flip-based repair after every successful insertion. + EveryInsertion, + /// Run local flip-based repair after every N successful insertions. + EveryN(NonZeroUsize), +} + +impl Default for DelaunayRepairPolicy { + #[inline] + fn default() -> Self { + Self::EveryInsertion + } +} + +impl DelaunayRepairPolicy { + /// Returns true if a repair pass should run after the given insertion count. + #[inline] + #[must_use] + pub const fn should_repair(self, insertion_count: usize) -> bool { + match self { + Self::Never => false, + Self::EveryInsertion => insertion_count != 0, + Self::EveryN(n) => insertion_count != 0 && insertion_count.is_multiple_of(n.get()), + } + } +} +/// Configuration for the optional heuristic rebuild fallback in Delaunay repair. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; +/// +/// let mut config = DelaunayRepairHeuristicConfig::default(); +/// config.shuffle_seed = Some(7); +/// config.perturbation_seed = Some(11); +/// assert_eq!(config.shuffle_seed, Some(7)); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[non_exhaustive] +pub struct DelaunayRepairHeuristicConfig { + /// Optional RNG seed used to shuffle vertex insertion order. + pub shuffle_seed: Option, + /// Optional seed used to vary the deterministic perturbation pattern. + pub perturbation_seed: Option, + /// Optional per-attempt flip budget cap. + /// + /// When set, each repair attempt is limited to at most this many flips. + /// `None` (the default) uses the dimension-dependent internal budget + /// computed from the triangulation size. + /// + /// This is primarily useful for debug harnesses that want to study + /// repair convergence behavior at different budgets without disabling + /// repair entirely. + pub max_flips: Option, +} + +impl DelaunayRepairHeuristicConfig { + /// Fills omitted seeds from a stable base so heuristic rebuilds are + /// repeatable even when callers only configure one axis of randomness. + pub(crate) fn resolve_seeds(self, base_seed: u64) -> DelaunayRepairHeuristicSeeds { + // Derive deterministic defaults when the caller does not provide explicit seeds. + const SHUFFLE_SALT: u64 = 0x9E37_79B9_7F4A_7C15; + const PERTURB_SALT: u64 = 0xD1B5_4A32_D192_ED03; + + let mut shuffle_seed = self + .shuffle_seed + .unwrap_or_else(|| base_seed.wrapping_add(SHUFFLE_SALT)); + if self.shuffle_seed.is_none() && shuffle_seed == 0 { + shuffle_seed = 1; + } + + let mut perturbation_seed = self + .perturbation_seed + .unwrap_or_else(|| base_seed.rotate_left(17) ^ PERTURB_SALT); + if self.perturbation_seed.is_none() && perturbation_seed == 0 { + perturbation_seed = 1; + } + + DelaunayRepairHeuristicSeeds { + shuffle_seed, + perturbation_seed, + } + } +} + +/// Seeds used for a heuristic rebuild. +/// +/// If the caller does not provide explicit seeds, deterministic defaults are derived from a stable +/// hash of the current vertex set. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::repair::DelaunayRepairHeuristicSeeds; +/// +/// let seeds = DelaunayRepairHeuristicSeeds { +/// shuffle_seed: 1, +/// perturbation_seed: 2, +/// }; +/// assert_eq!(seeds.shuffle_seed, 1); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DelaunayRepairHeuristicSeeds { + /// RNG seed used to shuffle vertex insertion order. + pub shuffle_seed: u64, + /// Seed used to vary the perturbation pattern during retries. + pub perturbation_seed: u64, +} + +/// Result of a flip-based repair attempt, including heuristic fallback metadata. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::repair::{ +/// DelaunayRepairOutcome, DelaunayRepairStats, +/// }; +/// +/// let outcome = DelaunayRepairOutcome { +/// stats: DelaunayRepairStats::default(), +/// heuristic: None, +/// }; +/// assert!(!outcome.used_heuristic()); +/// ``` +#[derive(Debug, Clone)] +pub struct DelaunayRepairOutcome { + /// Statistics from the final flip-based repair pass. + pub stats: DelaunayRepairStats, + /// Heuristic rebuild seeds, if a fallback was used. + pub heuristic: Option, +} + +impl DelaunayRepairOutcome { + /// Returns `true` if a heuristic rebuild fallback was used. + #[must_use] + pub const fn used_heuristic(&self) -> bool { + self.heuristic.is_some() + } +} + +/// Policy controlling when **global** Delaunay validation runs. +/// +/// This policy is **validation-only** (non-mutating) and is distinct from +/// [`DelaunayRepairPolicy`], which performs flip-based repairs. +/// +/// # ⚠️ Performance Warning +/// +/// Global Delaunay validation is **extremely expensive**: O(simplices × vertices). Use this policy +/// primarily when you need correctness guarantees and are willing to pay the cost. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::repair::DelaunayCheckPolicy; +/// use std::num::NonZeroUsize; +/// +/// # fn main() -> Result<(), Box> { +/// let every_three = NonZeroUsize::new(3).ok_or("check cadence must be non-zero")?; +/// let policy = DelaunayCheckPolicy::EveryN(every_three); +/// assert!(!policy.should_check(2)); +/// assert!(policy.should_check(3)); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum DelaunayCheckPolicy { + /// Run global Delaunay validation once after batch construction (e.g. `new()` / `with_kernel()`). + /// + /// Incremental insertion does not automatically run a final global check because there is no + /// intrinsic “end” signal; call + /// [`DelaunayTriangulation::is_valid`](crate::DelaunayTriangulation::is_valid) + /// or + /// [`DelaunayTriangulation::validate`](crate::DelaunayTriangulation::validate) + /// when you are done inserting. + #[default] + EndOnly, + /// Run global Delaunay validation after every N successful insertions. + EveryN(NonZeroUsize), +} + +impl DelaunayCheckPolicy { + /// Returns true if a global Delaunay validation pass should run after the given insertion count. + #[inline] + #[must_use] + pub const fn should_check(self, insertion_count: usize) -> bool { + match self { + Self::EndOnly => false, + Self::EveryN(n) => insertion_count.is_multiple_of(n.get()), + } + } +} + +// ============================================================================= +// REPAIR (Minimal Bounds) +// ============================================================================= + +impl DelaunayTriangulation +where + K: Kernel, +{ + /// Runs flip-based Delaunay repair over the full triangulation. + /// + /// This is a manual entrypoint that performs a global scan of interior facets + /// and applies k=2/k=3 bistellar flips until locally Delaunay or until the flip + /// budget is exhausted. On success, geometric orientation is re-canonicalized + /// to the positive sign. + /// + /// # Errors + /// + /// Returns a [`DelaunayRepairError`] if the repair fails to converge, an underlying + /// flip operation fails, or post-repair orientation canonicalization fails. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::repair::DelaunayRepairStats; + /// + /// 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 mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// let stats = dt.repair_delaunay_with_flips().unwrap(); + /// assert!(stats.facets_checked >= stats.flips_performed); + /// ``` + pub fn repair_delaunay_with_flips(&mut self) -> Result + where + K: ExactPredicates, + U: DataType, + V: DataType, + { + self.repair_delaunay_with_flips_capped(None) + } + + /// Runs flip-based repair with an optional per-attempt cap so public repair + /// and heuristic harnesses share one mutation path. + fn repair_delaunay_with_flips_capped( + &mut self, + max_flips: Option, + ) -> Result + where + K: ExactPredicates, + U: DataType, + V: DataType, + { + #[cfg(test)] + if test_hooks::force_repair_nonconvergent_enabled() { + return Err(test_hooks::synthetic_nonconvergent_error()); + } + let operation = TopologicalOperation::FacetFlip; + let topology = self.tri.topology_guarantee(); + if !operation.is_admissible_under(topology) { + return Err(DelaunayRepairError::InvalidTopology { + required: operation.required_topology(), + found: topology, + message: "Bistellar flips require a PL-manifold (vertex-link validation)", + }); + } + self.invalidate_locate_hint_cache(); + let (tds, kernel) = (&mut self.tri.tds, &self.tri.kernel); + let stats = repair_delaunay_with_flips_k2_k3(tds, kernel, None, topology, max_flips)?; + + // Re-canonicalize geometric orientation (#258): flip repair may leave + // the global sign negative. + self.ensure_positive_orientation()?; + + Ok(stats) + } + + /// Canonicalize geometric orientation to the positive sign, preserving + /// canonicalization failures as their own repair error variant. + fn ensure_positive_orientation(&mut self) -> Result<(), DelaunayRepairError> + where + U: DataType, + V: DataType, + { + self.tri + .normalize_and_promote_positive_orientation() + .map_err(|e| DelaunayRepairError::OrientationCanonicalizationFailed { + message: format!("after flip repair: {e}"), + }) + } + + /// Replays repair with an exact-predicate kernel before escalating to + /// heuristic rebuild. + fn repair_delaunay_with_flips_robust( + &mut self, + seed_simplices: Option<&[SimplexKey]>, + max_flips: Option, + ) -> Result + where + U: DataType, + V: DataType, + { + self.repair_delaunay_with_flips_robust_run(seed_simplices, max_flips) + .map(|run| run.stats) + } + + /// Replays repair with an exact-predicate kernel and returns the validation frontier. + pub(crate) fn repair_delaunay_with_flips_robust_run( + &mut self, + seed_simplices: Option<&[SimplexKey]>, + max_flips: Option, + ) -> Result + where + U: DataType, + V: DataType, + { + let topology = self.tri.topology_guarantee(); + let kernel = RobustKernel::::new(); + self.invalidate_locate_hint_cache(); + let (tds, kernel) = (&mut self.tri.tds, &kernel); + repair_delaunay_with_flips_k2_k3_run(tds, kernel, seed_simplices, topology, max_flips) + } + + /// Applies the repair policy only when the dimension and topology can + /// support bistellar flips. + pub(crate) fn should_run_delaunay_repair_for( + &self, + topology: TopologyGuarantee, + insertion_count: usize, + ) -> bool { + if D < 2 { + return false; + } + if self.tri.tds.number_of_simplices() == 0 { + return false; + } + + let policy = self.insertion_state.delaunay_repair_policy; + if policy == DelaunayRepairPolicy::Never { + return false; + } + + matches!( + policy.decide(insertion_count, topology, TopologicalOperation::FacetFlip), + RepairDecision::Proceed + ) + } + + /// Applies repair-policy and topology gates to non-insertion mutating operations. + /// + /// These operations do not have a meaningful insertion cadence, so every enabled + /// repair policy permits the post-mutation repair attempt. + pub(crate) fn should_run_delaunay_repair_after_mutation( + &self, + topology: TopologyGuarantee, + ) -> bool { + if D < 2 { + return false; + } + if self.tri.tds.number_of_simplices() == 0 { + return false; + } + if self.insertion_state.delaunay_repair_policy == DelaunayRepairPolicy::Never { + return false; + } + + TopologicalOperation::FacetFlip.is_admissible_under(topology) + } + + /// Enables test-only repair fallback paths without exposing a public knob. + #[cfg_attr( + not(test), + expect( + clippy::missing_const_for_fn, + reason = "runtime feature and environment checks should remain ordinary functions" + ) + )] + fn force_heuristic_rebuild_enabled() -> bool { + #[cfg(test)] + { + test_hooks::force_heuristic_rebuild_enabled() + } + #[cfg(not(test))] + { + false + } + } +} + +// ============================================================================= +// ADVANCED REPAIR & HEURISTIC REBUILD (Requires Numeric Scalar Bounds) +// ============================================================================= +// +// `repair_delaunay_with_flips_advanced` can fall back to `rebuild_with_heuristic`, +// which constructs a new triangulation and therefore adds `NumCast` on top of +// the scalar requirements guaranteed by `Kernel`. + +impl DelaunayTriangulation +where + K: Kernel, + K::Scalar: NumCast, + U: DataType, + V: DataType, +{ + /// Runs flip-based Delaunay repair + /// + /// This first attempts the standard two-pass flip repair. If it fails to converge (or if + /// the result cannot be verified as Delaunay), it rebuilds the triangulation from the + /// current vertex set using a shuffled insertion order and a perturbation seed, then runs + /// a final flip-repair pass. On success, geometric orientation is re-canonicalized + /// to the positive sign. + /// + /// The returned outcome marks whether the heuristic fallback was used and records + /// the seeds needed to reproduce it (if desired). + /// + /// # Errors + /// + /// Returns [`DelaunayRepairError`] if the flip-based repair fails, the heuristic + /// rebuild fallback cannot construct a valid triangulation, or post-repair + /// orientation canonicalization fails. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; + /// + /// 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 mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// let outcome = dt + /// .repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()) + /// .unwrap(); + /// assert!(outcome.stats.facets_checked >= outcome.stats.flips_performed); + /// ``` + pub fn repair_delaunay_with_flips_advanced( + &mut self, + config: DelaunayRepairHeuristicConfig, + ) -> Result + where + K: ExactPredicates, + { + if Self::force_heuristic_rebuild_enabled() { + let base_seed = self.heuristic_rebuild_base_seed(); + let seeds = config.resolve_seeds(base_seed); + let (candidate, stats, used_seeds) = + self.rebuild_with_heuristic(seeds, config.max_flips)?; + *self = candidate; + return Ok(DelaunayRepairOutcome { + stats, + heuristic: Some(used_seeds), + }); + } + let max_flips = config.max_flips; + match self.repair_delaunay_with_flips_capped(max_flips) { + Ok(stats) => Ok(DelaunayRepairOutcome { + stats, + heuristic: None, + }), + Err( + primary_err @ (DelaunayRepairError::NonConvergent { .. } + | DelaunayRepairError::PostconditionFailed { .. }), + ) => { + match self.repair_delaunay_with_flips_robust(None, max_flips) { + Ok(stats) => { + // Re-canonicalize geometric orientation (#258): robust flip + // repair may leave the global sign negative. + self.ensure_positive_orientation()?; + Ok(DelaunayRepairOutcome { + stats, + heuristic: None, + }) + } + Err(robust_err) => { + let base_seed = self.heuristic_rebuild_base_seed(); + let seeds = config.resolve_seeds(base_seed); + let (candidate, stats, used_seeds) = self + .rebuild_with_heuristic(seeds, max_flips) + .map_err(|heuristic_err| { + let heuristic_message = match heuristic_err { + DelaunayRepairError::HeuristicRebuildFailed { message } => { + message + } + other => other.to_string(), + }; + DelaunayRepairError::HeuristicRebuildFailed { + message: format!( + "primary repair failed ({primary_err}); robust fallback failed ({robust_err}); {heuristic_message}" + ), + } + })?; + *self = candidate; + Ok(DelaunayRepairOutcome { + stats, + heuristic: Some(used_seeds), + }) + } + } + } + Err(err) => Err(err), + } + } + + /// Rebuilds from the current vertex set with varied deterministic seeds when + /// flip repair cannot converge directly. + #[expect( + clippy::too_many_lines, + reason = "heuristic rebuild keeps point extraction, reconstruction, and validation together" + )] + fn rebuild_with_heuristic( + &self, + base_seeds: DelaunayRepairHeuristicSeeds, + max_flips_override: Option, + ) -> Result<(Self, DelaunayRepairStats, DelaunayRepairHeuristicSeeds), DelaunayRepairError> + where + K: ExactPredicates, + { + let base_vertices = self.collect_vertices_for_rebuild(); + + let mut last_error: Option = None; + + for attempt in 0..HEURISTIC_REBUILD_ATTEMPTS { + let seeds = if attempt == 0 { + base_seeds + } else { + // Vary the deterministic shuffle and perturbation patterns across attempts. + const SHUFFLE_SALT: u64 = 0x9E37_79B9_7F4A_7C15; + const PERTURB_SALT: u64 = 0xD1B5_4A32_D192_ED03; + + let attempt_u64 = attempt as u64; + + let mut shuffle_seed = base_seeds + .shuffle_seed + .wrapping_add(attempt_u64.wrapping_mul(SHUFFLE_SALT)); + if shuffle_seed == 0 { + shuffle_seed = 1; + } + + let mut perturbation_seed = + base_seeds.perturbation_seed ^ attempt_u64.wrapping_mul(PERTURB_SALT); + if perturbation_seed == 0 { + perturbation_seed = 1; + } + + DelaunayRepairHeuristicSeeds { + shuffle_seed, + perturbation_seed, + } + }; + + let rebuild_attempt = (|| { + let _guard = HeuristicRebuildRecursionGuard::enter(); + + // Shuffle vertices for this attempt. + let mut vertices = base_vertices.clone(); + let mut rng = rand::rngs::StdRng::seed_from_u64(seeds.shuffle_seed); + vertices.shuffle(&mut rng); + + // Heuristic rebuild is a last-resort fallback when global repair fails. Prefer an + // insertion schedule that keeps the triangulation near-Delaunay (local repairs on + // each insertion) so we do not get stuck in a non-regular configuration that flip + // repair cannot escape. + let topology_guarantee = self.tri.topology_guarantee(); + let global_topology = self.tri.global_topology(); + let mut candidate = Self::with_empty_kernel_and_topology_guarantee( + self.tri.kernel.clone(), + topology_guarantee, + ); + candidate.set_global_topology(global_topology); + + // During rebuild, force local repair after every insertion. We'll restore the caller's + // policies after we have a repaired candidate. + let rebuild_repair_policy = candidate.insertion_state.delaunay_repair_policy; + let rebuild_check_policy = candidate.insertion_state.delaunay_check_policy; + candidate.insertion_state.delaunay_repair_policy = + DelaunayRepairPolicy::EveryInsertion; + candidate.insertion_state.delaunay_check_policy = DelaunayCheckPolicy::EndOnly; + + for (idx, vertex) in vertices.into_iter().enumerate() { + let uuid = vertex.uuid(); + let coords = *vertex.point().coords(); + + let hint = candidate.insertion_state.last_inserted_simplex; + let insert_detail = { + let (tri, spatial_index) = + (&mut candidate.tri, &mut candidate.spatial_index); + tri.insert_with_statistics_seeded_indexed_detailed( + vertex, + None, + hint, + seeds.perturbation_seed, + spatial_index.as_mut(), + Some(idx), + ) + .map_err(|e| DelaunayRepairError::HeuristicRebuildFailed { + message: format!( + "heuristic rebuild insertion failed at idx={idx} uuid={uuid} coords={coords:?}: {e}" + ), + })? + }; + let repair_seed_simplices = insert_detail.repair_seed_simplices; + let delaunay_repair_required = insert_detail.delaunay_repair_required; + + match insert_detail.outcome { + InsertionOutcome::Inserted { vertex_key, hint } => { + candidate.insertion_state.last_inserted_simplex = hint; + candidate.insertion_state.delaunay_repair_insertion_count = candidate + .insertion_state + .delaunay_repair_insertion_count + .saturating_add(1); + + if delaunay_repair_required { + candidate + .maybe_repair_after_insertion_capped( + vertex_key, + hint, + &repair_seed_simplices, + max_flips_override, + ) + .map_err(|e| DelaunayRepairError::HeuristicRebuildFailed { + message: format!( + "heuristic rebuild repair failed at idx={idx} uuid={uuid} coords={coords:?}: {e}" + ), + })?; + } + + candidate + .maybe_check_after_insertion() + .map_err(|e| DelaunayRepairError::HeuristicRebuildFailed { + message: format!( + "heuristic rebuild Delaunay check failed at idx={idx} uuid={uuid} coords={coords:?}: {e}" + ), + })?; + } + InsertionOutcome::Skipped { error } => { + return Err(DelaunayRepairError::HeuristicRebuildFailed { + message: format!( + "heuristic rebuild skipped vertex at idx={idx} uuid={uuid} coords={coords:?}: {error}" + ), + }); + } + } + } + + candidate.tri.validation_policy = self.tri.validation_policy; + candidate.insertion_state.delaunay_repair_policy = + self.insertion_state.delaunay_repair_policy; + candidate.insertion_state.delaunay_check_policy = + self.insertion_state.delaunay_check_policy; + candidate.insertion_state.delaunay_repair_insertion_count = + self.insertion_state.delaunay_repair_insertion_count; + candidate.insertion_state.last_inserted_simplex = None; + + // Restore prior rebuild-only policies (kept for completeness; currently overwritten above). + let _ = (rebuild_repair_policy, rebuild_check_policy); + + let topology = candidate.tri.topology_guarantee(); + candidate.invalidate_locate_hint_cache(); + let (tds, kernel) = (&mut candidate.tri.tds, &candidate.tri.kernel); + let stats = repair_delaunay_with_flips_k2_k3( + tds, + kernel, + None, + topology, + max_flips_override, + )?; + + // Re-canonicalize geometric orientation (#258): the final flip + // repair may leave the global sign negative. + candidate.ensure_positive_orientation()?; + + Ok::<_, DelaunayRepairError>((candidate, stats)) + })(); + + match rebuild_attempt { + Ok((candidate, stats)) => return Ok((candidate, stats, seeds)), + Err(err) => { + last_error = Some(format!( + "attempt {}/{} (shuffle_seed={} perturbation_seed={}): {err}", + attempt + 1, + HEURISTIC_REBUILD_ATTEMPTS, + seeds.shuffle_seed, + seeds.perturbation_seed, + )); + } + } + } + + Err(DelaunayRepairError::HeuristicRebuildFailed { + message: format!( + "heuristic rebuild failed after {HEURISTIC_REBUILD_ATTEMPTS} attempts: {}", + last_error.unwrap_or_else(|| "unknown error".to_string()) + ), + }) + } + + /// Preserves vertex UUIDs and data so heuristic rebuilds remain an internal + /// repair strategy, not a user-visible remapping. + fn collect_vertices_for_rebuild(&self) -> Vec> { + self.tri + .tds + .vertices() + .map(|(_, vertex)| Vertex::new_with_uuid(*vertex.point(), vertex.uuid(), vertex.data)) + .collect() + } + + /// Derives rebuild seeds from the vertex set so fallback behavior is + /// reproducible regardless of slotmap iteration accidents. + fn heuristic_rebuild_base_seed(&self) -> u64 { + let mut vertex_hashes = Vec::with_capacity(self.tri.tds.number_of_vertices()); + for (_, vertex) in self.tri.tds.vertices() { + let mut hasher = FastHasher::default(); + vertex.hash(&mut hasher); + vertex_hashes.push(hasher.finish()); + } + vertex_hashes.sort_unstable(); + stable_hash_u64_slice(&vertex_hashes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::construction::test_hooks; + use crate::core::algorithms::flips::{ + DelaunayRepairDiagnostics, FlipError, RepairQueueOrder, verify_delaunay_via_flip_predicates, + }; + use crate::core::simplex::Simplex; + use crate::core::tds::{Tds, TriangulationConstructionState}; + use crate::core::validation::TopologyGuarantee; + use crate::core::vertex::Vertex; + use crate::geometry::kernel::{AdaptiveKernel, RobustKernel}; + use crate::topology::traits::topological_space::{GlobalTopology, ToroidalConstructionMode}; + use crate::triangulation::DelaunayTriangulation; + use crate::vertex; + use std::{num::NonZeroUsize, sync::Once}; + + fn init_tracing() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_test_writer() + .try_init(); + }); + } + + struct ForceHeuristicRebuildGuard { + prior: bool, + } + + impl ForceHeuristicRebuildGuard { + fn enable() -> Self { + let prior = test_hooks::set_force_heuristic_rebuild(true); + Self { prior } + } + } + + impl Drop for ForceHeuristicRebuildGuard { + fn drop(&mut self) { + test_hooks::restore_force_heuristic_rebuild(self.prior); + } + } + + struct ForceRepairNonconvergentGuard { + prior: bool, + } + + impl ForceRepairNonconvergentGuard { + fn enable() -> Self { + let prior = test_hooks::set_force_repair_nonconvergent(true); + Self { prior } + } + } + + impl Drop for ForceRepairNonconvergentGuard { + fn drop(&mut self) { + test_hooks::restore_force_repair_nonconvergent(self.prior); + } + } + + fn non_delaunay_quad_tds() -> Tds { + let mut tds: Tds = Tds::empty(); + let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let v1 = tds.insert_vertex_with_mapping(vertex!([4.0, 0.0])).unwrap(); + let v2 = tds.insert_vertex_with_mapping(vertex!([4.0, 2.0])).unwrap(); + let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 2.0])).unwrap(); + + tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) + .unwrap(); + tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v2, v3], None).unwrap()) + .unwrap(); + tds.construction_state = TriangulationConstructionState::Constructed; + tds.assign_neighbors().unwrap(); + tds.assign_incident_simplices().unwrap(); + tds + } + + // ========================================================================= + // Delaunay repair helper methods + // ========================================================================= + + #[test] + fn test_should_run_delaunay_repair_for_skips_for_dimension_lt_2() { + init_tracing(); + let vertices: Vec> = vec![vertex!([0.0]), vertex!([1.0])]; + let dt: DelaunayTriangulation<_, (), (), 1> = + DelaunayTriangulation::new(&vertices).unwrap(); + + assert_eq!(dt.number_of_simplices(), 1); + assert_eq!( + dt.delaunay_repair_policy(), + DelaunayRepairPolicy::EveryInsertion + ); + assert!(!dt.should_run_delaunay_repair_for(dt.topology_guarantee(), 1)); + } + + #[test] + fn test_should_run_delaunay_repair_for_skips_when_no_simplices() { + init_tracing(); + let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::empty(); + + assert_eq!(dt.number_of_simplices(), 0); + assert_eq!( + dt.delaunay_repair_policy(), + DelaunayRepairPolicy::EveryInsertion + ); + assert!(!dt.should_run_delaunay_repair_for(dt.topology_guarantee(), 1)); + } + + #[test] + fn test_should_run_delaunay_repair_for_skips_when_policy_never() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + assert_eq!(dt.number_of_simplices(), 1); + dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); + assert!(!dt.should_run_delaunay_repair_for(dt.topology_guarantee(), 1)); + } + + #[test] + fn test_should_run_delaunay_repair_for_respects_every_n_schedule() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + dt.set_delaunay_repair_policy(DelaunayRepairPolicy::EveryN(NonZeroUsize::new(2).unwrap())); + let topology = dt.topology_guarantee(); + + assert!(!dt.should_run_delaunay_repair_for(topology, 0)); + assert!(!dt.should_run_delaunay_repair_for(topology, 1)); + assert!(dt.should_run_delaunay_repair_for(topology, 2)); + } + + #[test] + fn test_non_insertion_mutation_repair_gate_ignores_insertion_cadence() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + let topology = dt.topology_guarantee(); + + dt.set_delaunay_repair_policy(DelaunayRepairPolicy::EveryN(NonZeroUsize::new(2).unwrap())); + assert!(dt.should_run_delaunay_repair_after_mutation(topology)); + + dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); + assert!(!dt.should_run_delaunay_repair_after_mutation(topology)); + } + + #[test] + fn test_vertex_key_valid_after_explicit_heuristic_rebuild() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([1.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Insert a vertex normally (no heuristic rebuild during insert). + let inserted = vertex!([0.25, 0.25]); + let inserted_uuid = inserted.uuid(); + + let (outcome, _stats) = dt.insert_with_statistics(inserted).unwrap(); + let InsertionOutcome::Inserted { vertex_key, .. } = outcome else { + panic!("Expected successful insertion outcome"); + }; + + // Force a heuristic rebuild via the public repair API. + let _guard = ForceHeuristicRebuildGuard::enable(); + let outcome = dt + .repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()) + .unwrap(); + assert!( + outcome.used_heuristic(), + "Expected heuristic rebuild to be used" + ); + + // Verify the vertex is still findable by UUID after heuristic rebuild. + let remapped = dt + .tri + .tds + .vertex_key_from_uuid(&inserted_uuid) + .expect("Inserted vertex UUID missing after heuristic rebuild"); + + // The vertex key may have changed after heuristic rebuild, but the + // vertex should still be present and accessible. + assert!(dt.tri.tds.vertex(remapped).is_some()); + assert!(dt.validate().is_ok()); + // Original vertex_key may be stale after heuristic rebuild; that is + // expected. The important invariant is that the UUID lookup works. + let _ = vertex_key; + } + + #[test] + fn test_heuristic_rebuild_preserves_global_topology() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([1.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + let global_topology = GlobalTopology::Toroidal { + domain: [1.0, 1.0], + mode: ToroidalConstructionMode::PeriodicImagePoint, + }; + dt.set_global_topology(global_topology); + + let _guard = ForceHeuristicRebuildGuard::enable(); + let outcome = dt + .repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()) + .unwrap(); + + assert!( + outcome.used_heuristic(), + "Expected forced heuristic rebuild to be used" + ); + assert_eq!(dt.global_topology(), global_topology); + assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); + } + + #[test] + fn test_repair_delaunay_with_flips_allows_pl_manifold() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([1.0, 1.0]), + ]; + + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + dt.set_topology_guarantee(TopologyGuarantee::PLManifold); + + let result = dt.repair_delaunay_with_flips(); + assert!( + !matches!(result, Err(DelaunayRepairError::InvalidTopology { .. })), + "Flip-based repair should be admissible under PLManifold topology" + ); + } + + /// When the primary flip repair returns `NonConvergent`, the advanced repair + /// method falls back to `repair_delaunay_with_flips_robust`. On a valid + /// triangulation the robust pass succeeds, so the outcome reports no + /// heuristic rebuild. + #[test] + fn test_repair_delaunay_with_flips_advanced_robust_fallback_succeeds() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([1.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + let _guard = ForceRepairNonconvergentGuard::enable(); + let outcome = dt + .repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()) + .unwrap(); + assert!( + !outcome.used_heuristic(), + "Robust fallback should succeed without needing heuristic rebuild" + ); + } + + /// Verifies that `DelaunayRepairHeuristicConfig::max_flips` caps the repair budget + /// when called through the public `repair_delaunay_with_flips_advanced` API. + /// + /// Sub-case 1: A budget of 0 on a triangulation that is already Delaunay should succeed + /// (the initial repair pass finds no violations). + /// + /// Sub-case 2: A budget of 0 on a forced-non-convergent state should hit the + /// robust fallback path (the primary pass returns `NonConvergent`, the robust + /// pass succeeds because the triangulation is actually Delaunay). + #[test] + fn test_repair_advanced_max_flips_zero_on_valid_triangulation_succeeds() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([1.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Sub-case 1: Already Delaunay — max_flips=0 should succeed (no flips needed). + let config = DelaunayRepairHeuristicConfig { + max_flips: Some(0), + ..DelaunayRepairHeuristicConfig::default() + }; + let outcome = dt.repair_delaunay_with_flips_advanced(config).unwrap(); + assert_eq!(outcome.stats.flips_performed, 0); + assert!( + !outcome.used_heuristic(), + "Already-Delaunay triangulation should not trigger heuristic rebuild" + ); + } + + /// Sub-case 2 of the `max_flips` budget test: force the primary repair to fail + /// (via `ForceRepairNonconvergentGuard`) with `max_flips=0`, then verify the + /// robust fallback succeeds (the triangulation is actually valid). + #[test] + fn test_repair_advanced_max_flips_zero_forced_nonconvergent_hits_robust_fallback() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + vertex!([1.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + + let _guard = ForceRepairNonconvergentGuard::enable(); + let config = DelaunayRepairHeuristicConfig { + max_flips: Some(0), + ..DelaunayRepairHeuristicConfig::default() + }; + // The primary repair is forced to fail; the robust fallback should succeed + // because the triangulation is actually Delaunay. + let outcome = dt.repair_delaunay_with_flips_advanced(config).unwrap(); + assert_eq!( + outcome.stats.flips_performed, 0, + "max_flips=0 should prevent any flips even on the robust fallback path" + ); + assert!( + !outcome.used_heuristic(), + "Robust fallback should succeed without heuristic rebuild" + ); + } + + /// Sub-case 3: + /// verify `max_flips=0` returns `NonConvergent`, then retry with a sufficient budget + /// and verify repair succeeds with flips performed. + #[test] + fn test_repair_advanced_max_flips_on_non_delaunay_triangulation() { + init_tracing(); + + // Reuse the explicit non-Delaunay quadrilateral fixture so the primary + // 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> = + DelaunayTriangulation::from_tds_with_topology_guarantee( + tds, + kernel, + TopologyGuarantee::PLManifold, + ); + dt.set_topology_guarantee(TopologyGuarantee::PLManifold); + + // max_flips=0 should fail (flips are needed but budget is zero). + let config_zero = DelaunayRepairHeuristicConfig { + max_flips: Some(0), + ..DelaunayRepairHeuristicConfig::default() + }; + // The advanced path tries primary (fails at budget=0), then robust fallback. + // The robust fallback also respects the budget, so it should also fail at 0, + // then the heuristic rebuild fires. The key assertion: it should not silently + // succeed with 0 flips on the primary path. + let outcome_zero = dt.repair_delaunay_with_flips_advanced(config_zero); + // Either heuristic rebuild succeeds or we get an error — both are acceptable. + // What would be wrong is a silent Ok with 0 flips on a non-Delaunay input. + if let Ok(ref outcome) = outcome_zero { + assert!( + outcome.used_heuristic() || outcome.stats.flips_performed > 0, + "max_flips=0 on non-Delaunay input must not silently succeed with 0 flips and no heuristic" + ); + } + + // Now retry with a generous budget — should succeed. + let config_generous = DelaunayRepairHeuristicConfig { + max_flips: Some(100), + ..DelaunayRepairHeuristicConfig::default() + }; + // Reconstruct dt from the same raw TDS in case the previous attempt mutated it. + let tds2 = non_delaunay_quad_tds(); + let mut dt2: DelaunayTriangulation, (), (), 2> = + DelaunayTriangulation::from_tds_with_topology_guarantee( + tds2, + AdaptiveKernel::new(), + TopologyGuarantee::PLManifold, + ); + dt2.set_topology_guarantee(TopologyGuarantee::PLManifold); + let outcome_generous = dt2 + .repair_delaunay_with_flips_advanced(config_generous) + .unwrap(); + assert!( + outcome_generous.stats.flips_performed > 0, + "Generous budget should allow flips to repair the non-Delaunay triangulation" + ); + } + + /// `repair_delaunay_with_flips` delegates to `repair_delaunay_with_flips_k2_k3` + /// which requires D ≥ 2. On a 1D triangulation the inner function returns + /// `FlipError::UnsupportedDimension`, surfaced as `DelaunayRepairError::Flip`. + #[test] + fn test_repair_delaunay_with_flips_returns_flip_error_for_1d() { + init_tracing(); + let vertices: Vec> = vec![vertex!([0.0]), vertex!([1.0])]; + let mut dt: DelaunayTriangulation, (), (), 1> = + DelaunayTriangulation::new(&vertices).unwrap(); + + let result = dt.repair_delaunay_with_flips(); + assert!( + matches!( + result, + Err(DelaunayRepairError::Flip(FlipError::UnsupportedDimension { + dimension: 1 + })) + ), + "Expected Flip(UnsupportedDimension {{ dimension: 1 }}) for D=1, got: {result:?}" + ); + } + + /// `repair_delaunay_with_flips_advanced` passes through non-retryable errors + /// (anything other than `NonConvergent` / `PostconditionFailed`) from the + /// inner `repair_delaunay_with_flips` call. A 1D triangulation triggers + /// `UnsupportedDimension` which must hit the `Err(err) => Err(err)` arm. + #[test] + fn test_repair_delaunay_with_flips_advanced_passes_through_non_retryable_error() { + init_tracing(); + let vertices: Vec> = vec![vertex!([0.0]), vertex!([1.0])]; + let mut dt: DelaunayTriangulation, (), (), 1> = + DelaunayTriangulation::new(&vertices).unwrap(); + + let result = + dt.repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig::default()); + assert!( + matches!( + result, + Err(DelaunayRepairError::Flip(FlipError::UnsupportedDimension { + dimension: 1 + })) + ), + "Expected non-retryable Flip(UnsupportedDimension) pass-through for D=1, got: {result:?}" + ); + } + + // ---- advanced repair fallback-chain error context tests ---- + + /// Verify that the `HeuristicRebuildFailed` error from + /// `repair_delaunay_with_flips_advanced` includes the full fallback + /// chain context (primary, robust, and heuristic failures) when all + /// three stages fail. + #[test] + fn test_advanced_repair_fallback_error_preserves_full_chain_context() { + // Construct the error exactly the way `repair_delaunay_with_flips_advanced` + // builds it when all three stages fail. + let primary_err = DelaunayRepairError::NonConvergent { + max_flips: 1000, + diagnostics: Box::new(DelaunayRepairDiagnostics { + facets_checked: 50, + flips_performed: 1000, + max_queue_len: 42, + 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, + }), + }; + let robust_err = DelaunayRepairError::PostconditionFailed { + message: "robust postcondition failure".to_string(), + }; + let heuristic_inner = DelaunayRepairError::HeuristicRebuildFailed { + message: "heuristic rebuild failed after 3 attempts: attempt 3/3 (shuffle_seed=1 perturbation_seed=2): inner".to_string(), + }; + + // Simulate the map_err closure in repair_delaunay_with_flips_advanced. + let heuristic_message = match heuristic_inner { + DelaunayRepairError::HeuristicRebuildFailed { message } => message, + other => other.to_string(), + }; + let combined = DelaunayRepairError::HeuristicRebuildFailed { + message: format!( + "primary repair failed ({primary_err}); robust fallback failed ({robust_err}); {heuristic_message}" + ), + }; + + let msg = combined.to_string(); + assert!( + msg.contains("primary repair failed"), + "error should mention primary failure: {msg}" + ); + assert!( + msg.contains("robust fallback failed"), + "error should mention robust failure: {msg}" + ); + assert!( + msg.contains("robust postcondition failure"), + "error should include robust failure details: {msg}" + ); + assert!( + msg.contains("heuristic rebuild failed after 3 attempts"), + "error should include heuristic rebuild details: {msg}" + ); + } + #[test] + fn repair_operation_display_describes_mutation() { + assert_eq!( + DelaunayRepairOperation::VertexRemoval.to_string(), + "vertex removal" + ); + } + + #[test] + fn check_policy_end_only_never_checks_during_insertion() { + assert!(!DelaunayCheckPolicy::EndOnly.should_check(0)); + assert!(!DelaunayCheckPolicy::EndOnly.should_check(1)); + } + + #[test] + fn check_policy_every_n_checks_on_multiples() { + let every_2 = DelaunayCheckPolicy::EveryN(NonZeroUsize::new(2).unwrap()); + + assert!(every_2.should_check(0)); + assert!(!every_2.should_check(1)); + assert!(every_2.should_check(2)); + assert!(!every_2.should_check(3)); + assert!(every_2.should_check(4)); + } + + #[test] + fn repair_policy_zero_insertions_never_repairs() { + assert!(!DelaunayRepairPolicy::EveryInsertion.should_repair(0)); + assert!(!DelaunayRepairPolicy::EveryN(NonZeroUsize::new(2).unwrap()).should_repair(0)); + assert!(!DelaunayRepairPolicy::Never.should_repair(0)); + } + + #[test] + fn repair_policy_every_insertion_skips_never_and_repairs_after_first_insertion() { + assert!(!DelaunayRepairPolicy::Never.should_repair(1)); + assert!(DelaunayRepairPolicy::EveryInsertion.should_repair(1)); + assert!(DelaunayRepairPolicy::EveryInsertion.should_repair(17)); + } + + #[test] + fn repair_policy_every_n_repairs_only_on_nonzero_multiples() { + let every_3 = DelaunayRepairPolicy::EveryN(NonZeroUsize::new(3).unwrap()); + + assert!(!every_3.should_repair(1)); + assert!(!every_3.should_repair(2)); + assert!(every_3.should_repair(3)); + assert!(!every_3.should_repair(4)); + assert!(every_3.should_repair(6)); + } + + #[test] + fn heuristic_config_resolves_missing_seeds_deterministically() { + let config = DelaunayRepairHeuristicConfig { + shuffle_seed: None, + perturbation_seed: Some(11), + max_flips: Some(7), + }; + + let seeds = config.resolve_seeds(5); + + assert_ne!(seeds.shuffle_seed, 0); + assert_eq!(seeds.perturbation_seed, 11); + } + + #[test] + fn heuristic_config_keeps_explicit_zero_seeds() { + let config = DelaunayRepairHeuristicConfig { + shuffle_seed: Some(0), + perturbation_seed: Some(0), + max_flips: None, + }; + + let seeds = config.resolve_seeds(0); + + assert_eq!(seeds.shuffle_seed, 0); + assert_eq!(seeds.perturbation_seed, 0); + } + + #[test] + fn repair_outcome_reports_whether_heuristic_was_used() { + let without_heuristic = DelaunayRepairOutcome { + stats: DelaunayRepairStats::default(), + heuristic: None, + }; + let with_heuristic = DelaunayRepairOutcome { + stats: DelaunayRepairStats::default(), + heuristic: Some(DelaunayRepairHeuristicSeeds { + shuffle_seed: 1, + perturbation_seed: 2, + }), + }; + + assert!(!without_heuristic.used_heuristic()); + assert!(with_heuristic.used_heuristic()); + } +} diff --git a/src/delaunay/serialization.rs b/src/delaunay/serialization.rs new file mode 100644 index 00000000..b89064cd --- /dev/null +++ b/src/delaunay/serialization.rs @@ -0,0 +1,184 @@ +//! Serialization support for Delaunay triangulations. + +#![forbid(unsafe_code)] + +use crate::core::tds::Tds; +use crate::core::traits::data_type::DataType; +use crate::geometry::kernel::{Kernel, RobustKernel}; +use crate::triangulation::DelaunayTriangulation; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +impl Serialize for DelaunayTriangulation +where + K: Kernel, + U: DataType, + V: DataType, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.tri.tds.serialize(serializer) + } +} + +/// Custom `Deserialize` implementation for `RobustKernel` with no custom data. +/// +/// Kernels are stateless and can be reconstructed on deserialization. This +/// implementation only serializes the `Tds`, which contains all geometric and +/// topological data, then reconstructs the kernel wrapper on deserialization. +/// +/// # Note on Locate Hint Persistence +/// +/// The internal `insertion_state.last_inserted_simplex` locate hint is not +/// serialized. Deserialization reconstructs a fresh triangulation via +/// [`DelaunayTriangulation::try_from_tds`], which resets the hint to `None`. +/// This only affects performance for the first few insertions after loading. +/// +/// # Usage with Other Kernels +/// +/// For other kernels such as `AdaptiveKernel` or `FastKernel`, or custom data +/// types, deserialize the `Tds` directly and reconstruct with +/// [`DelaunayTriangulation::try_from_tds`]: +/// +/// ```rust +/// # use delaunay::prelude::geometry::*; +/// # use delaunay::prelude::tds::Tds; +/// # use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +/// # fn example() { +/// 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>::new(&vertices) +/// .expect("nondegenerate tetrahedron should construct"); +/// let json = serde_json::to_string(&dt).expect("triangulation should serialize"); +/// +/// let tds: Tds = +/// serde_json::from_str(&json).expect("serialized triangulation should deserialize"); +/// let dt_adaptive = DelaunayTriangulation::try_from_tds(tds, AdaptiveKernel::new()) +/// .expect("deserialized TDS should validate"); +/// # } +/// ``` +impl<'de, const D: usize> Deserialize<'de> for DelaunayTriangulation, (), (), D> +where + Tds: Deserialize<'de>, +{ + fn deserialize(deserializer: De) -> Result + where + De: Deserializer<'de>, + { + let tds = Tds::deserialize(deserializer)?; + Self::try_from_tds(tds, RobustKernel::new()).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::simplex::Simplex; + use crate::core::tds::TriangulationConstructionState; + use crate::geometry::kernel::AdaptiveKernel; + use crate::vertex; + use std::sync::Once; + + fn init_tracing() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_test_writer() + .try_init(); + }); + } + + fn non_delaunay_quad_tds() -> Tds { + let mut tds: Tds = Tds::empty(); + let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let v1 = tds.insert_vertex_with_mapping(vertex!([4.0, 0.0])).unwrap(); + let v2 = tds.insert_vertex_with_mapping(vertex!([4.0, 2.0])).unwrap(); + let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 2.0])).unwrap(); + + tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) + .unwrap(); + tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v2, v3], None).unwrap()) + .unwrap(); + tds.construction_state = TriangulationConstructionState::Constructed; + tds.assign_neighbors().unwrap(); + tds.assign_incident_simplices().unwrap(); + tds + } + + #[test] + fn robust_deserialize_rejects_non_delaunay_connectivity() { + init_tracing(); + let json = serde_json::to_string(&non_delaunay_quad_tds()).unwrap(); + + let err = + serde_json::from_str::, (), (), 2>>(&json) + .expect_err("serde reconstruction must reject non-Delaunay connectivity"); + + let message = err.to_string(); + assert!( + message.contains("Delaunay verification failed"), + "serde error should preserve the Level 4 validation failure: {message}" + ); + } + + #[test] + fn serde_roundtrip_uses_custom_deserialize_impl() { + init_tracing(); + let vertices = [ + 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::new(&vertices).unwrap(); + + let json = serde_json::to_string(&dt).unwrap(); + + let tds: Tds = serde_json::from_str(&json).unwrap(); + let roundtrip_adaptive = + DelaunayTriangulation::try_from_tds(tds, AdaptiveKernel::new()).unwrap(); + + assert_eq!( + roundtrip_adaptive.number_of_vertices(), + dt.number_of_vertices() + ); + assert_eq!( + roundtrip_adaptive.number_of_simplices(), + dt.number_of_simplices() + ); + assert!( + roundtrip_adaptive + .insertion_state + .last_inserted_simplex + .is_none() + ); + + let roundtrip_robust: DelaunayTriangulation, (), (), 3> = + serde_json::from_str(&json).unwrap(); + + assert_eq!( + roundtrip_robust.number_of_vertices(), + dt.number_of_vertices() + ); + assert_eq!( + roundtrip_robust.number_of_simplices(), + dt.number_of_simplices() + ); + assert!( + roundtrip_robust + .insertion_state + .last_inserted_simplex + .is_none() + ); + } +} diff --git a/src/delaunay/triangulation.rs b/src/delaunay/triangulation.rs new file mode 100644 index 00000000..0d2050a7 --- /dev/null +++ b/src/delaunay/triangulation.rs @@ -0,0 +1,81 @@ +//! Delaunay triangulation layer with incremental insertion. +//! +//! This layer adds Delaunay-specific operations on top of the generic +//! `Triangulation` struct, following CGAL's architecture. + +#![forbid(unsafe_code)] + +use crate::core::collections::spatial_hash_grid::HashGridIndex; +use crate::core::operations::DelaunayInsertionState; +use crate::core::triangulation::Triangulation; +use crate::geometry::kernel::Kernel; + +/// Delaunay triangulation with incremental insertion support. +/// +/// # Type Parameters +/// - `K`: Geometric kernel implementing predicates +/// - `U`: User data type for vertices +/// - `V`: User data type for simplices +/// - `D`: Dimension of the triangulation +/// +/// # Delaunay Property Note +/// +/// The triangulation satisfies **structural validity** (all TDS invariants) and +/// uses **flip-based repairs** to restore the local Delaunay property after insertion. +/// By default, k=2/k=3 bistellar flip queues run automatically after each successful +/// insertion (see [`DelaunayRepairPolicy`](crate::DelaunayRepairPolicy)). +/// +/// For applications requiring explicit verification, you can still call +/// [`is_valid`](Self::is_valid) (Level 4) or [`validate`](Self::validate) (Levels 1–4). +/// If flip-based repair fails to converge, insertion returns an error and the +/// triangulation is left structurally valid but not guaranteed Delaunay. +/// +/// See: [Issue #120 Investigation](https://github.com/acgetchell/delaunay/blob/main/docs/archive/issue_120_investigation.md) +/// +/// # Implementation +/// +/// 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`] +/// - ✅ 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 +/// +/// ```rust +/// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +/// +/// 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::new(&vertices).unwrap(); +/// +/// assert_eq!(dt.number_of_simplices(), 1); +/// ``` +#[derive(Clone, Debug)] +pub struct DelaunayTriangulation, U, V, const D: usize> { + /// The underlying generic triangulation. + pub(crate) tri: Triangulation, + /// Ephemeral insertion/repair state (hint caching + repair scheduling). + pub(crate) insertion_state: DelaunayInsertionState, + /// Optional spatial hash-grid index used to accelerate duplicate detection and locate-hint + /// selection during incremental insertion. + /// + /// This is a performance-only cache and is not serialized; it may be rebuilt lazily. + /// Query paths validate returned vertex keys against the live TDS, so the + /// cache can survive transactional rollbacks even if they leave behind stale + /// keys from an insertion that did not commit. + pub(crate) spatial_index: Option>, +} diff --git a/src/delaunay/validation.rs b/src/delaunay/validation.rs new file mode 100644 index 00000000..66f62624 --- /dev/null +++ b/src/delaunay/validation.rs @@ -0,0 +1,951 @@ +//! Validation scheduling helpers for triangulation construction diagnostics. +//! +//! This module contains validation-control concepts that are orthogonal to the +//! Delaunay data structure itself. Keeping them here leaves +//! the crate root focused on construction, repair, and query logic. + +#![forbid(unsafe_code)] + +use crate::core::algorithms::flips::{DelaunayRepairError, verify_delaunay_for_triangulation}; +use crate::core::operations::DelaunayInsertionState; +use crate::core::tds::{ + InvariantError, InvariantKind, InvariantViolation, Tds, TdsError, TriangulationValidationReport, +}; +use crate::core::traits::data_type::DataType; +use crate::core::triangulation::Triangulation; +use crate::core::util::is_delaunay_property_only; +use crate::core::validation::{TopologyGuarantee, TriangulationValidationError}; +use crate::geometry::kernel::Kernel; +use crate::repair::DelaunayRepairOperation; +use crate::topology::traits::topological_space::GlobalTopology; +use crate::triangulation::DelaunayTriangulation; +use std::num::NonZeroUsize; +use thiserror::Error; + +/// Errors that can occur during Delaunay triangulation validation and repair. +/// +/// The first three variants are returned by [`DelaunayTriangulation::validate`](crate::DelaunayTriangulation::validate) +/// (validation Levels 1–4): +/// - [`Tds`](Self::Tds) — element or TDS structural errors (Levels 1–2). +/// - [`Triangulation`](Self::Triangulation) — topology errors (Level 3). +/// - [`VerificationFailed`](Self::VerificationFailed) — Delaunay property violation (Level 4). +/// +/// [`DelaunayTriangulation::is_valid`](crate::DelaunayTriangulation::is_valid) returns only the Level 4 +/// [`VerificationFailed`](Self::VerificationFailed) variant. +/// +/// The repair-failure variants are **not** returned by `validate()` or +/// `is_valid()`. They are produced by mutating operations that invoke +/// flip-based repair internally (e.g. [`DelaunayTriangulation::remove_vertex`](crate::DelaunayTriangulation::remove_vertex)). +/// +/// When manually forwarding lower-layer validation errors, prefer +/// `DelaunayTriangulationValidationError::from(tds_error)` or `.into()` for +/// [`TdsError`] and [`TriangulationValidationError`]. The enum stores those +/// sources behind `Box` to keep `Result<_, DelaunayTriangulationValidationError>` +/// compact while preserving typed error inspection. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; +/// use delaunay::prelude::validation::DelaunayTriangulationValidationError; +/// +/// # fn main() -> Result<(), Box> { +/// 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::new(&vertices)?; +/// +/// let result: Result<(), DelaunayTriangulationValidationError> = dt.validate(); +/// assert!(result.is_ok()); +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum DelaunayTriangulationValidationError { + /// Lower-layer element or TDS structural validation error (Levels 1–2). + #[error(transparent)] + Tds(Box), + + /// Lower-layer topology validation error (Level 3). + #[error(transparent)] + Triangulation(Box), + + /// Flip-based Delaunay verification detected a violation. + /// + /// This is returned by [`DelaunayTriangulation::is_valid`](crate::DelaunayTriangulation::is_valid) when the fast + /// O(simplices) flip-predicate scan finds a Delaunay violation. The error is + /// a Level 4 (Delaunay property) issue, not a Level 1–2 structural problem. + #[error("Delaunay verification failed: {message}")] + VerificationFailed { + /// Description of the verification failure. + message: String, + }, + + /// Flip-based Delaunay repair failed with string-only context. + /// + /// This variant is retained for compatibility with existing callers. New + /// mutating operations that can preserve the repair source should prefer + /// [`RepairOperationFailed`](Self::RepairOperationFailed). + /// + /// **Not** returned by `validate()` or `is_valid()` — those use + /// [`VerificationFailed`](Self::VerificationFailed) for passive checks. + #[error("Delaunay repair failed: {message}")] + RepairFailed { + /// Description of the repair failure. + message: String, + }, + + /// Flip-based Delaunay repair failed during a specific mutating operation. + /// + /// This preserves the underlying [`DelaunayRepairError`] so callers can + /// inspect budget exhaustion, topology errors, predicate failures, and other + /// repair causes without parsing display text. Operations that report this + /// variant are responsible for documenting whether failure is transactional; + /// [`remove_vertex`](crate::DelaunayTriangulation::remove_vertex) + /// restores the pre-removal triangulation when post-removal repair fails. + /// + /// **Not** returned by `validate()` or `is_valid()` — those use + /// [`VerificationFailed`](Self::VerificationFailed) for passive checks. + #[error("Delaunay repair failed during {operation}: {source}")] + RepairOperationFailed { + /// Mutating operation that invoked repair. + operation: DelaunayRepairOperation, + /// Underlying flip-repair failure. + #[source] + source: Box, + }, +} + +impl From for DelaunayTriangulationValidationError { + fn from(source: TdsError) -> Self { + Self::Tds(Box::new(source)) + } +} + +impl From for DelaunayTriangulationValidationError { + fn from(source: TriangulationValidationError) -> Self { + Self::Triangulation(Box::new(source)) + } +} + +/// Cadence for explicit validation checkpoints during construction diagnostics. +/// +/// This is separate from [`crate::ValidationPolicy`], +/// which controls automatic insertion-time validation inside +/// [`crate::Triangulation`]. Diagnostic +/// harnesses can use this cadence for explicit periodic +/// [`DelaunayTriangulation::is_valid`](crate::DelaunayTriangulation::is_valid) +/// checks without overloading repair policy or exposing raw `Option` +/// scheduling in logs. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::validation::ValidationCadence; +/// +/// let cadence = ValidationCadence::from_optional_every(Some(128)); +/// assert!(!cadence.should_validate(0)); +/// assert!(!cadence.should_validate(127)); +/// assert!(cadence.should_validate(128)); +/// ``` +#[must_use = "validation cadence values only affect diagnostics when they are used"] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValidationCadence { + /// Disable explicit periodic validation checkpoints. + Never, + /// Run explicit validation every N successful insertion attempts. + EveryN(NonZeroUsize), +} + +impl ValidationCadence { + /// Converts an optional integer cadence into a typed validation cadence. + /// + /// `None` and `Some(0)` disable periodic validation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::validation::ValidationCadence; + /// + /// assert!(matches!( + /// ValidationCadence::from_optional_every(Some(32)), + /// ValidationCadence::EveryN(every) if every.get() == 32, + /// )); + /// assert_eq!( + /// ValidationCadence::from_optional_every(None), + /// ValidationCadence::Never, + /// ); + /// ``` + pub const fn from_optional_every(validate_every: Option) -> Self { + match validate_every { + None | Some(0) => Self::Never, + Some(every) => { + if let Some(every) = NonZeroUsize::new(every) { + Self::EveryN(every) + } else { + Self::Never + } + } + } + } + + /// Returns true when validation should run for a one-based insertion count. + /// + /// A count of `0` never triggers validation because no insertion has + /// completed yet. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::validation::ValidationCadence; + /// + /// let cadence = ValidationCadence::from_optional_every(Some(4)); + /// assert!(!cadence.should_validate(0)); + /// assert!(!cadence.should_validate(3)); + /// assert!(cadence.should_validate(4)); + /// ``` + #[must_use] + pub const fn should_validate(self, insertion_count: usize) -> bool { + match self { + Self::Never => false, + Self::EveryN(every) => { + insertion_count != 0 && insertion_count.is_multiple_of(every.get()) + } + } + } +} + +// ============================================================================= +// VALIDATION (Minimal Bounds) +// ============================================================================= + +impl DelaunayTriangulation +where + K: Kernel, + U: DataType, + V: DataType, +{ + // ------------------------------------------------------------------------- + // VALIDATION + // ------------------------------------------------------------------------- + + /// Validates the Delaunay empty-circumsphere property (Level 4). + /// + /// This is the Delaunay layer's `is_valid`: it checks **only** the Delaunay property + /// and intentionally does **not** run lower-layer validation. + /// + /// **Performance**: Uses fast O(simplices) flip-based verification instead of the naive + /// O(simplices × vertices) brute-force check, providing ~40-100x speedup. This method is + /// correct for all properly-constructed triangulations (which is the standard case). + /// + /// For cumulative validation across the whole hierarchy, use [`validate`](Self::validate). + /// + /// # Errors + /// + /// Returns a [`DelaunayTriangulationValidationError`] if the empty-circumsphere test fails, or if + /// the underlying triangulation state is inconsistent and prevents geometric predicates + /// from being evaluated. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// let vertices_4d = [ + /// vertex!([0.0, 0.0, 0.0, 0.0]), + /// vertex!([1.0, 0.0, 0.0, 0.0]), + /// vertex!([0.0, 1.0, 0.0, 0.0]), + /// vertex!([0.0, 0.0, 1.0, 0.0]), + /// vertex!([0.0, 0.0, 0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 4> = + /// DelaunayTriangulation::new(&vertices_4d).unwrap(); + /// + /// // Level 4: Delaunay property only + /// assert!(dt.is_valid().is_ok()); + /// ``` + pub fn is_valid(&self) -> Result<(), DelaunayTriangulationValidationError> { + // Use fast flip-based verification (O(simplices) instead of O(simplices × vertices)) + self.is_delaunay_via_flips().map_err(|err| { + DelaunayTriangulationValidationError::VerificationFailed { + message: err.to_string(), + } + }) + } + + /// Verify the Delaunay property via fast O(simplices) flip predicates. + /// + /// This checks the Delaunay property by testing all possible flip configurations + /// (k=2 facets, k=3 ridges, and their inverses) instead of the naive O(simplices × vertices) + /// brute-force check. This is ~40-100x faster while being equally correct. + /// + /// Ideal for property-based testing with many iterations. + /// + /// # Errors + /// + /// Returns [`DelaunayRepairError`] if any flip predicate detects a Delaunay violation. + /// + /// # Examples + /// + /// ``` + /// use delaunay::prelude::query::*; + /// + /// 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::new(&vertices).unwrap(); + /// + /// // Fast O(N) verification + /// assert!(dt.is_delaunay_via_flips().is_ok()); + /// ``` + pub fn is_delaunay_via_flips(&self) -> Result<(), DelaunayRepairError> { + verify_delaunay_for_triangulation(&self.tri) + } + + /// Performs cumulative validation for Levels 1–4. + /// + /// This validates: + /// - **Levels 1–3** via [`Triangulation::validate`](crate::Triangulation::validate) + /// - **Level 4** via [`DelaunayTriangulation::is_valid`](Self::is_valid) + /// + /// # Errors + /// + /// Returns a [`DelaunayTriangulationValidationError`] if Levels 1–3 validation fails or if the + /// Delaunay property check (Level 4) fails. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// let vertices_4d = [ + /// vertex!([0.0, 0.0, 0.0, 0.0]), + /// vertex!([1.0, 0.0, 0.0, 0.0]), + /// vertex!([0.0, 1.0, 0.0, 0.0]), + /// vertex!([0.0, 0.0, 1.0, 0.0]), + /// vertex!([0.0, 0.0, 0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 4> = + /// DelaunayTriangulation::new(&vertices_4d).unwrap(); + /// + /// // Levels 1–4: elements + structure + topology + Delaunay property + /// assert!(dt.validate().is_ok()); + /// ``` + pub fn validate(&self) -> Result<(), DelaunayTriangulationValidationError> { + self.tri.validate().map_err(|e| match e { + InvariantError::Tds(tds_err) => tds_err.into(), + InvariantError::Triangulation(tri_err) => tri_err.into(), + InvariantError::Delaunay(dt_err) => dt_err, + })?; + self.is_valid() + } + + /// Generate a comprehensive validation report for the full validation hierarchy. + /// + /// This is intended for debugging/telemetry (e.g. `insert_with_statistics`) where + /// you want to see *all* violated invariants, not just the first one. + /// + /// # Notes + /// - If UUID↔key mappings are inconsistent, this returns only mapping failures (other + /// checks may produce misleading secondary errors). + /// - This report is **cumulative** across Levels 1–4. + /// + /// # Errors + /// + /// Returns `Err(TriangulationValidationReport)` containing all violated invariants. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::query::*; + /// + /// let vertices = [ + /// 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::new(&vertices).unwrap(); + /// + /// // Returns Ok(()) on success; otherwise returns a report listing all violations. + /// let report = dt.validation_report(); + /// assert!(report.is_ok()); + /// ``` + pub fn validation_report(&self) -> Result<(), TriangulationValidationReport> { + // Levels 1–3: reuse the Triangulation layer report. + match self.tri.validation_report() { + Ok(()) => { + // Level 4 (Delaunay property) + if let Err(e) = self.is_valid() { + return Err(TriangulationValidationReport { + violations: vec![InvariantViolation { + kind: InvariantKind::DelaunayProperty, + error: e.into(), + }], + }); + } + Ok(()) + } + Err(mut report) => { + // If mappings are inconsistent, return the lower-layer report unchanged. + if report.violations.iter().any(|v| { + matches!( + v.kind, + InvariantKind::VertexMappings | InvariantKind::SimplexMappings + ) + }) { + return Err(report); + } + + // Level 4 (Delaunay property) + if let Err(e) = self.is_delaunay_via_flips() { + report.violations.push(InvariantViolation { + kind: InvariantKind::DelaunayProperty, + error: InvariantError::Delaunay( + DelaunayTriangulationValidationError::VerificationFailed { + message: e.to_string(), + }, + ), + }); + } + + if report.violations.is_empty() { + Ok(()) + } else { + Err(report) + } + } + } + } + // ------------------------------------------------------------------------- + // PURE STRUCT ASSEMBLY + // ------------------------------------------------------------------------- + /// Create a validated `DelaunayTriangulation` from a `Tds` with an explicit kernel. + /// + /// This is useful when you've serialized just the `Tds` and want to reconstruct + /// the `DelaunayTriangulation` with a caller-supplied kernel. The `kernel` + /// parameter provides the geometric predicates used during validation and later + /// insertions. + /// + /// # Notes + /// + /// - The internal `insertion_state.last_inserted_simplex` "locate hint" is intentionally **not** persisted + /// across serialization boundaries. Reconstructing via `try_from_tds` (including the serde + /// `Deserialize` impl below) always resets it to `None`. This can make the first few + /// insertions after loading slightly slower, but is otherwise behaviorally irrelevant. + /// - The internal spatial hash-grid index used to accelerate incremental insertion is also a + /// performance-only cache and is not serialized. Reconstructing via `try_from_tds` leaves it unset + /// so it can be rebuilt lazily on demand. + /// - The topology guarantee ([`TopologyGuarantee`]) is also not serialized (this type serializes + /// only the `Tds`). Reconstructing via `try_from_tds` resets it to `TopologyGuarantee::DEFAULT` + /// (currently `PLManifold`). Call [`set_topology_guarantee`](Self::set_topology_guarantee) + /// after loading if you want to relax to `Pseudomanifold` for performance, or use + /// [`try_from_tds_with_topology_guarantee`](Self::try_from_tds_with_topology_guarantee) to set it + /// at construction time. + /// - Runtime global topology metadata ([`GlobalTopology`]) is also not serialized. Reconstructing + /// via `try_from_tds` validates with [`GlobalTopology::Euclidean`]. Use + /// [`try_from_tds_with_topology_context`](Self::try_from_tds_with_topology_context) if you + /// need to validate toroidal or other non-default topology metadata during reconstruction. + /// - Euclidean reconstruction validates Level 4 with the crate's robust + /// empty-circumsphere validator, independent of the supplied runtime kernel. + /// The supplied kernel is stored for later queries and insertions. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::geometry::FastKernel; + /// use delaunay::prelude::tds::Tds; + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0, 0.0, 0.0]), + /// vertex!([1.0, 0.0, 0.0, 0.0]), + /// vertex!([0.0, 1.0, 0.0, 0.0]), + /// vertex!([0.0, 0.0, 1.0, 0.0]), + /// vertex!([0.0, 0.0, 0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 4> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// // Serialize just the Tds + /// let json = serde_json::to_string(dt.tds()).unwrap(); + /// + /// // Deserialize Tds and reconstruct DelaunayTriangulation + /// let tds: Tds = serde_json::from_str(&json).unwrap(); + /// let reconstructed = DelaunayTriangulation::try_from_tds(tds, FastKernel::new()).unwrap(); + /// assert_eq!(reconstructed.number_of_vertices(), 5); + /// ``` + /// + /// # Errors + /// + /// Returns [`DelaunayTriangulationValidationError`] if the TDS violates + /// structural, topological, or Delaunay invariants. + pub fn try_from_tds( + tds: Tds, + kernel: K, + ) -> Result { + Self::try_from_tds_with_topology_context( + tds, + kernel, + TopologyGuarantee::DEFAULT, + GlobalTopology::DEFAULT, + ) + } + + /// Create a validated `DelaunayTriangulation` from a `Tds` with an explicit topology guarantee. + /// + /// The candidate is assembled with the requested guarantee, then validated + /// at Levels 1–4 before being returned. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::geometry::FastKernel; + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulation, TopologyGuarantee, vertex, + /// }; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 2> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// let reconstructed = DelaunayTriangulation::try_from_tds_with_topology_guarantee( + /// dt.tds().clone(), + /// FastKernel::new(), + /// TopologyGuarantee::PLManifoldStrict, + /// ) + /// .unwrap(); + /// + /// assert_eq!( + /// reconstructed.topology_guarantee(), + /// TopologyGuarantee::PLManifoldStrict + /// ); + /// ``` + /// + /// # Errors + /// + /// Returns [`DelaunayTriangulationValidationError`] if the TDS violates + /// structural, topological, or Delaunay invariants. + pub fn try_from_tds_with_topology_guarantee( + tds: Tds, + kernel: K, + topology_guarantee: TopologyGuarantee, + ) -> Result { + Self::try_from_tds_with_topology_context( + tds, + kernel, + topology_guarantee, + GlobalTopology::DEFAULT, + ) + } + + /// Create a validated `DelaunayTriangulation` from a `Tds` with explicit topology context. + /// + /// This is the checked reconstruction path for serialized TDS data whose + /// runtime [`TopologyGuarantee`] or [`GlobalTopology`] metadata must be + /// restored before validation. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::geometry::FastKernel; + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulation, GlobalTopology, TopologyGuarantee, vertex, + /// }; + /// + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let dt: DelaunayTriangulation<_, (), (), 2> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// let reconstructed = DelaunayTriangulation::try_from_tds_with_topology_context( + /// dt.tds().clone(), + /// FastKernel::new(), + /// TopologyGuarantee::PLManifoldStrict, + /// GlobalTopology::Euclidean, + /// ) + /// .unwrap(); + /// + /// assert_eq!( + /// reconstructed.topology_guarantee(), + /// TopologyGuarantee::PLManifoldStrict + /// ); + /// assert_eq!(reconstructed.global_topology(), GlobalTopology::Euclidean); + /// ``` + /// + /// # Errors + /// + /// Returns [`DelaunayTriangulationValidationError`] if the TDS violates + /// structural, topological, or Delaunay invariants under the supplied + /// topology context. + pub fn try_from_tds_with_topology_context( + tds: Tds, + kernel: K, + topology_guarantee: TopologyGuarantee, + global_topology: GlobalTopology, + ) -> Result { + let mut candidate = Self::from_tds_with_topology_guarantee(tds, kernel, topology_guarantee); + candidate.set_global_topology(global_topology); + candidate.tri.validate().map_err(|e| match e { + InvariantError::Tds(tds_err) => tds_err.into(), + InvariantError::Triangulation(tri_err) => tri_err.into(), + InvariantError::Delaunay(dt_err) => dt_err, + })?; + + if candidate.global_topology().is_euclidean() { + is_delaunay_property_only(&candidate.tri.tds).map_err(|e| { + DelaunayTriangulationValidationError::VerificationFailed { + message: format!("kernel-independent reconstruction validation failed: {e}"), + } + })?; + } else { + candidate.is_valid()?; + } + Ok(candidate) + } + + /// Assemble a `DelaunayTriangulation` from a `Tds` with an explicit topology guarantee. + /// + /// This crate-internal constructor performs no validation; public callers + /// must use [`try_from_tds_with_topology_guarantee`](Self::try_from_tds_with_topology_guarantee). + /// The initial + /// [`ValidationPolicy`](crate::ValidationPolicy) is derived from the guarantee: + /// [`PLManifoldStrict`](TopologyGuarantee::PLManifoldStrict) uses + /// [`Always`](crate::ValidationPolicy::Always); all others default to + /// [`OnSuspicion`](crate::ValidationPolicy::OnSuspicion). + #[must_use] + pub(crate) const fn from_tds_with_topology_guarantee( + tds: Tds, + kernel: K, + topology_guarantee: TopologyGuarantee, + ) -> Self { + let validation_policy = topology_guarantee.default_validation_policy(); + Self { + tri: Triangulation { + kernel, + tds, + global_topology: GlobalTopology::DEFAULT, + validation_policy, + topology_guarantee, + }, + insertion_state: DelaunayInsertionState::new(), + spatial_index: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::algorithms::flips::{DelaunayRepairDiagnostics, RepairQueueOrder}; + use crate::core::simplex::Simplex; + use crate::core::tds::{SimplexKey, TriangulationConstructionState, VertexKey}; + use crate::geometry::kernel::AdaptiveKernel; + use crate::vertex; + use std::{error::Error, sync::Once}; + use uuid::Uuid; + + fn init_tracing() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + let filter = tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")); + let _ = tracing_subscriber::fmt() + .with_env_filter(filter) + .with_test_writer() + .try_init(); + }); + } + + fn non_delaunay_quad_tds() -> Tds { + let mut tds: Tds = Tds::empty(); + let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let v1 = tds.insert_vertex_with_mapping(vertex!([4.0, 0.0])).unwrap(); + let v2 = tds.insert_vertex_with_mapping(vertex!([4.0, 2.0])).unwrap(); + let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 2.0])).unwrap(); + + tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap()) + .unwrap(); + tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v2, v3], None).unwrap()) + .unwrap(); + tds.construction_state = TriangulationConstructionState::Constructed; + tds.assign_neighbors().unwrap(); + tds.assign_incident_simplices().unwrap(); + tds + } + + #[test] + fn validation_cadence_maps_optional_every() { + assert_eq!( + ValidationCadence::from_optional_every(None), + ValidationCadence::Never + ); + assert_eq!( + ValidationCadence::from_optional_every(Some(0)), + ValidationCadence::Never + ); + assert_eq!( + ValidationCadence::from_optional_every(Some(128)), + ValidationCadence::EveryN(NonZeroUsize::new(128).unwrap()) + ); + } + + #[test] + fn validation_cadence_should_validate_on_multiples() { + let cadence = ValidationCadence::EveryN(NonZeroUsize::new(64).unwrap()); + + assert!(!cadence.should_validate(0)); + assert!(!cadence.should_validate(63)); + assert!(cadence.should_validate(64)); + assert!(!cadence.should_validate(65)); + assert!(cadence.should_validate(128)); + assert!(!ValidationCadence::Never.should_validate(64)); + } + + #[test] + fn verification_failed_display_includes_context() { + let err = DelaunayTriangulationValidationError::VerificationFailed { + message: "flip predicate detected non-Delaunay facet".to_string(), + }; + let msg = err.to_string(); + + assert!( + msg.contains("Delaunay verification failed"), + "Display should contain prefix: {msg}" + ); + assert!( + msg.contains("flip predicate detected non-Delaunay facet"), + "Display should contain inner message: {msg}" + ); + } + + #[test] + fn repair_operation_failed_preserves_source() { + let source = DelaunayRepairError::NonConvergent { + max_flips: 7, + diagnostics: Box::new(DelaunayRepairDiagnostics { + facets_checked: 3, + flips_performed: 7, + max_queue_len: 5, + 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, + }), + }; + let err = DelaunayTriangulationValidationError::RepairOperationFailed { + operation: DelaunayRepairOperation::VertexRemoval, + source: Box::new(source), + }; + + let msg = err.to_string(); + assert!(msg.contains("vertex removal")); + match &err { + DelaunayTriangulationValidationError::RepairOperationFailed { + operation: DelaunayRepairOperation::VertexRemoval, + source, + } if matches!( + source.as_ref(), + DelaunayRepairError::NonConvergent { max_flips: 7, .. } + ) => {} + other => panic!("expected typed vertex-removal repair source, got {other:?}"), + } + let chained = err + .source() + .expect("typed repair failure should expose source error") + .to_string(); + assert!(chained.contains("failed to converge after 7 flips")); + } + + #[test] + fn tds_variant_display_delegates_to_source() { + let inner = TdsError::InconsistentDataStructure { + message: "broken link".to_string(), + }; + let err = DelaunayTriangulationValidationError::from(inner); + + assert!(err.to_string().contains("broken link")); + } + + #[test] + fn triangulation_variant_display_delegates_to_source() { + let inner = TriangulationValidationError::IsolatedVertex { + vertex_key: VertexKey::from(slotmap::KeyData::from_ffi(1)), + vertex_uuid: Uuid::nil(), + }; + let err = DelaunayTriangulationValidationError::from(inner); + + assert!(err.to_string().contains("Isolated vertex")); + } + + #[test] + fn try_from_tds_rejects_non_delaunay_connectivity() { + init_tracing(); + let tds = non_delaunay_quad_tds(); + + let err = DelaunayTriangulation::try_from_tds(tds, AdaptiveKernel::new()) + .expect_err("checked TDS reconstruction must reject non-Delaunay connectivity"); + + assert!( + matches!( + err, + DelaunayTriangulationValidationError::VerificationFailed { .. } + ), + "expected Level 4 validation failure, got {err:?}" + ); + } + + #[test] + fn test_validation_report_ok_for_valid_triangulation() { + init_tracing(); + let vertices = [ + 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::new(&vertices).unwrap(); + assert!(dt.validation_report().is_ok()); + } + + #[test] + fn test_validation_report_returns_mapping_failures_only() { + init_tracing(); + let vertices = [ + 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 mut dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Break UUID↔key mappings: remove one vertex UUID entry. + let uuid = dt.tri.tds.vertices().next().unwrap().1.uuid(); + dt.tri.tds.uuid_to_vertex_key.remove(&uuid); + + let report = dt.validation_report().unwrap_err(); + assert!(!report.violations.is_empty()); + assert!(report.violations.iter().all(|v| { + matches!( + v.kind, + InvariantKind::VertexMappings | InvariantKind::SimplexMappings + ) + })); + + // Early-return on mapping failures: do not add derived invariants. + assert!( + report + .violations + .iter() + .all(|v| v.kind != InvariantKind::DelaunayProperty) + ); + } + + #[test] + fn test_validation_report_includes_vertex_incidence_violation() { + init_tracing(); + let vertices = [ + 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 mut dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Corrupt a `Vertex::incident_simplex` pointer. + let vertex_key = dt.tri.tds.vertices().next().unwrap().0; + dt.tri + .tds + .vertex_mut(vertex_key) + .unwrap() + .set_incident_simplex(Some(SimplexKey::default())); + + let report = dt.validation_report().unwrap_err(); + assert!( + report + .violations + .iter() + .any(|v| v.kind == InvariantKind::VertexIncidence) + ); + } + + #[test] + fn test_dt_validate_maps_tds_error_to_tds_variant() { + init_tracing(); + let vertices = [ + 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 mut dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // 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); + + match dt.validate() { + Err(DelaunayTriangulationValidationError::Tds(source)) + if matches!(source.as_ref(), TdsError::MappingInconsistency { .. }) => {} + other => panic!( + "Expected DelaunayTriangulationValidationError::Tds(MappingInconsistency), got {other:?}" + ), + } + } + + #[test] + fn test_dt_validate_maps_topology_error_to_triangulation_variant() { + init_tracing(); + let vertices = [ + 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 mut dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + + // Add an isolated vertex so Level 3 (topology) fails. + let _ = dt + .tds_mut() + .insert_vertex_with_mapping(vertex!([0.5, 0.5, 0.5])) + .unwrap(); + + match dt.validate() { + Err(DelaunayTriangulationValidationError::Triangulation(source)) + if matches!( + source.as_ref(), + TriangulationValidationError::IsolatedVertex { .. } + ) => {} + other => panic!( + "Expected DelaunayTriangulationValidationError::Triangulation(IsolatedVertex), got {other:?}" + ), + } + } +} diff --git a/src/geometry/algorithms/convex_hull.rs b/src/geometry/algorithms/convex_hull.rs index 82c2eb39..e896c0b4 100644 --- a/src/geometry/algorithms/convex_hull.rs +++ b/src/geometry/algorithms/convex_hull.rs @@ -18,11 +18,11 @@ //! # #[derive(Debug, thiserror::Error)] //! # enum ExampleError { //! # #[error(transparent)] -//! # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), +//! # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), //! # #[error(transparent)] //! # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), //! # #[error(transparent)] -//! # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), +//! # Insertion(#[from] delaunay::prelude::InsertionError), //! # #[error("expected tetrahedron hull to have a first facet")] //! # MissingFacet, //! # } @@ -296,17 +296,17 @@ pub enum ConvexHullConstructionError { /// Use `is_valid_for_triangulation()` to check if a hull is still valid for a given TDS: /// /// ```rust -/// # use delaunay::prelude::triangulation::DelaunayTriangulation; +/// # use delaunay::prelude::DelaunayTriangulation; /// # use delaunay::prelude::query::ConvexHull; /// # use delaunay::vertex; /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] -/// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), +/// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] -/// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), +/// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -337,18 +337,18 @@ pub enum ConvexHullConstructionError { /// ## Example: Correct Usage Pattern /// /// ```rust -/// use delaunay::prelude::triangulation::DelaunayTriangulation; +/// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] -/// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), +/// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] -/// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), +/// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -454,18 +454,18 @@ impl ConvexHull { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -503,18 +503,18 @@ impl ConvexHull { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -547,18 +547,18 @@ impl ConvexHull { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error(transparent)] /// # Facet(#[from] delaunay::prelude::tds::FacetError), /// # #[error("expected tetrahedron hull to have a first facet")] @@ -600,18 +600,18 @@ impl ConvexHull { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -646,18 +646,18 @@ impl ConvexHull { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -716,18 +716,18 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -888,18 +888,18 @@ impl ConvexHull { /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -964,18 +964,18 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -1109,7 +1109,7 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::prelude::geometry::Point; /// use delaunay::prelude::geometry::Coordinate; @@ -1118,11 +1118,11 @@ where /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -1509,7 +1509,7 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::prelude::geometry::Point; /// use delaunay::prelude::geometry::Coordinate; @@ -1518,11 +1518,11 @@ where /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -1598,7 +1598,7 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::prelude::geometry::Point; /// use delaunay::prelude::geometry::Coordinate; @@ -1607,11 +1607,11 @@ where /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -1734,7 +1734,7 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::prelude::geometry::Point; /// use delaunay::prelude::geometry::Coordinate; @@ -1743,11 +1743,11 @@ where /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -1797,18 +1797,18 @@ where /// # Examples /// /// ```rust - /// use delaunay::prelude::triangulation::DelaunayTriangulation; + /// use delaunay::prelude::DelaunayTriangulation; /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { /// # #[error(transparent)] - /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # Construction(#[from] delaunay::prelude::DelaunayTriangulationConstructionError), /// # #[error(transparent)] /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), /// # #[error(transparent)] - /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # Insertion(#[from] delaunay::prelude::InsertionError), /// # #[error("expected tetrahedron hull to have a first facet")] /// # MissingFacet, /// # } @@ -1952,15 +1952,16 @@ pub type ConvexHull4D = ConvexHull; #[cfg(test)] mod tests { use super::*; + use crate::construction::{ + DelaunayConstructionFailure, DelaunayTriangulationConstructionError, + }; use crate::core::algorithms::incremental_insertion::InsertionError; use crate::core::tds::{Tds, TdsError}; use crate::core::traits::facet_cache::FacetCacheProvider; use crate::core::util::{checked_facet_key_from_vertex_keys, facet_view_to_vertices}; use crate::geometry::kernel::AdaptiveKernel; use crate::geometry::traits::coordinate::CoordinateConversionError; - use crate::triangulation::delaunay::{ - DelaunayConstructionFailure, DelaunayTriangulation, DelaunayTriangulationConstructionError, - }; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use std::error::Error; use std::sync::atomic::Ordering; diff --git a/src/geometry/kernel.rs b/src/geometry/kernel.rs index 8656d398..6777b556 100644 --- a/src/geometry/kernel.rs +++ b/src/geometry/kernel.rs @@ -331,7 +331,7 @@ impl_exact_predicates_for_supported_dims!(0, 1, 2, 3, 4, 5); /// /// Use [`AdaptiveKernel`] (the default) for all 3D+ work. `FastKernel` remains /// suitable for 2D triangulations with well-conditioned input, or when explicitly -/// opted into via [`DelaunayTriangulation::with_kernel`](crate::triangulation::delaunay::DelaunayTriangulation::with_kernel) for advanced use cases +/// opted into via [`DelaunayTriangulation::with_kernel`](crate::DelaunayTriangulation::with_kernel) for advanced use cases /// where the caller has verified the input is non-degenerate. /// /// # Performance @@ -559,7 +559,7 @@ where /// This is the **default kernel** for [`DelaunayTriangulation`] convenience /// constructors (`new`, `empty`, `new_with_options`, etc.). /// -/// [`DelaunayTriangulation`]: crate::triangulation::delaunay::DelaunayTriangulation +/// [`DelaunayTriangulation`]: crate::DelaunayTriangulation /// /// # When to use `AdaptiveKernel` /// diff --git a/src/geometry/quality.rs b/src/geometry/quality.rs index c456e328..5bce23f0 100644 --- a/src/geometry/quality.rs +++ b/src/geometry/quality.rs @@ -528,14 +528,15 @@ where #[cfg(test)] mod tests { use super::*; + use crate::construction::{ + DelaunayConstructionFailure, DelaunayTriangulationConstructionError, + }; use crate::core::simplex::Simplex; use crate::core::tds::Tds; use crate::core::triangulation::Triangulation; use crate::geometry::kernel::{AdaptiveKernel, FastKernel}; use crate::geometry::traits::coordinate::Coordinate; - use crate::triangulation::delaunay::{ - DelaunayConstructionFailure, DelaunayTriangulation, DelaunayTriangulationConstructionError, - }; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use approx::assert_relative_eq; diff --git a/src/geometry/util/measures.rs b/src/geometry/util/measures.rs index fc013588..41155e85 100644 --- a/src/geometry/util/measures.rs +++ b/src/geometry/util/measures.rs @@ -745,7 +745,7 @@ mod tests { use crate::core::traits::boundary_analysis::BoundaryAnalysis; use crate::core::vertex::Vertex; use crate::geometry::point::Point; - use crate::triangulation::delaunay::DelaunayTriangulation; + use crate::triangulation::DelaunayTriangulation; use crate::vertex; use approx::assert_relative_eq; diff --git a/src/geometry/util/triangulation_generation.rs b/src/geometry/util/triangulation_generation.rs index 7d2dd371..0485f268 100644 --- a/src/geometry/util/triangulation_generation.rs +++ b/src/geometry/util/triangulation_generation.rs @@ -6,17 +6,19 @@ #![forbid(unsafe_code)] use super::point_generation::{generate_random_points, generate_random_points_seeded}; +use crate::construction::{ + ConstructionOptions, DelaunayTriangulationConstructionError, InsertionOrderStrategy, + RetryPolicy, +}; +use crate::core::construction::TriangulationConstructionError; use crate::core::simplex::SimplexValidationError; use crate::core::traits::data_type::DataType; -use crate::core::triangulation::{TopologyGuarantee, TriangulationConstructionError}; +use crate::core::validation::TopologyGuarantee; use crate::core::vertex::Vertex; use crate::geometry::kernel::{AdaptiveKernel, Kernel}; use crate::geometry::point::Point; use crate::geometry::traits::coordinate::CoordinateScalar; -use crate::triangulation::delaunay::{ - ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationConstructionError, - InsertionOrderStrategy, RetryPolicy, -}; +use crate::triangulation::DelaunayTriangulation; use rand::SeedableRng; use rand::distr::uniform::SampleUniform; use rand::rngs::StdRng; @@ -330,7 +332,7 @@ where /// /// ```no_run /// use delaunay::prelude::generators::generate_random_triangulation_with_topology_guarantee; -/// use delaunay::prelude::triangulation::TopologyGuarantee; +/// use delaunay::prelude::TopologyGuarantee; /// /// let dt = generate_random_triangulation_with_topology_guarantee::( /// 20, @@ -466,7 +468,7 @@ where /// ```no_run /// use delaunay::prelude::generators::RandomTriangulationBuilder; /// use delaunay::prelude::generators::InsertionOrderStrategy; -/// use delaunay::prelude::triangulation::TopologyGuarantee; +/// use delaunay::prelude::TopologyGuarantee; /// /// // Override the default `Hilbert` ordering with `Input` ordering. /// let dt = RandomTriangulationBuilder::new(20, (-3.0, 3.0)) @@ -506,7 +508,7 @@ impl RandomTriangulationBuilder { /// - Default construction options ([`InitialSimplexStrategy::MaxVolume`] initial simplex, /// shuffled retries, no explicit deduplication) /// - /// [`InitialSimplexStrategy::MaxVolume`]: crate::triangulation::delaunay::InitialSimplexStrategy::MaxVolume + /// [`InitialSimplexStrategy::MaxVolume`]: crate::construction::InitialSimplexStrategy::MaxVolume /// /// # Examples /// diff --git a/src/lib.rs b/src/lib.rs index 6f80bb43..a38ee5f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,27 +49,39 @@ //! //! | Task | Import | //! |---|---| -//! | Construct/configure a Delaunay triangulation | `use delaunay::prelude::triangulation::construction::*` | -//! | Low-level incremental insertion building blocks | `use delaunay::prelude::triangulation::insertion::*` | +//! | 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::*` | //! | Read-only queries, traversal, convex hull | `use delaunay::prelude::query::*` | //! | Point location and conflict-region algorithms | `use delaunay::prelude::algorithms::*` | //! | Geometry helpers, predicates, points | `use delaunay::prelude::geometry::*` | //! | Random points / triangulations for examples and tests | `use delaunay::prelude::generators::*` | //! | Hilbert ordering and quantization utilities | `use delaunay::prelude::ordering::*` | -//! | Bistellar flips (Pachner moves) | `use delaunay::prelude::triangulation::flips::*` | -//! | Delaunay repair and flip-based Level 4 validation | `use delaunay::prelude::triangulation::repair::*` | -//! | Delaunayize workflow (repair + flip) | `use delaunay::prelude::triangulation::delaunayize::*` | -//! | Construction telemetry diagnostics | `use delaunay::prelude::triangulation::diagnostics::*` | -//! | Construction validation cadence/policy | `use delaunay::prelude::triangulation::validation::*` | +//! | Bistellar flips (Pachner moves) | `use delaunay::prelude::flips::*` | +//! | Delaunay repair and flip-based Level 4 validation | `use delaunay::prelude::repair::*` | +//! | Delaunayize workflow (repair + flip) | `use delaunay::prelude::delaunayize::*` | +//! | Construction telemetry diagnostics | `use delaunay::prelude::diagnostics::*` | +//! | Construction validation cadence/policy | `use delaunay::prelude::validation::*` | //! | Topology validation, Euler characteristic | `use delaunay::prelude::topology::validation::*` | //! | Topological spaces and topology traits | `use delaunay::prelude::topology::spaces::*` | //! | Low-level TDS simplices, facets, keys | `use delaunay::prelude::tds::*` | //! | Collection types (`FastHashMap`, etc.) | `use delaunay::prelude::collections::*` | -//! | Legacy broad triangulation import | `use delaunay::prelude::triangulation::*` | -//! | Everything (kitchen sink) | `use delaunay::prelude::*` | +//! | Broad convenience import for exploratory code | `use delaunay::prelude::*` | //! //! ## Public low-level namespace policy //! +//! High-level Delaunay APIs are available directly from the crate root and +//! focused root modules: [`DelaunayTriangulation`], [`DelaunayTriangulationBuilder`], +//! [`construction`](crate::construction), [`flips`](crate::flips), +//! [`repair`](crate::repair), [`validation`](crate::validation), and +//! [`delaunayize`](crate::delaunayize). The nested `delaunay::delaunay::*` +//! facade is intentionally not part of the public API; use the crate root or a +//! focused prelude instead. +//! +//! ```compile_fail +//! use delaunay::delaunay::DelaunayTriangulation; +//! ``` +//! //! The low-level implementation namespace is private. The public low-level //! surface is exposed through curated modules: //! [`tds`](crate::tds), [`collections`](crate::collections), @@ -87,10 +99,10 @@ //! ### Validation hierarchy (Levels 1–4) //! //! ```rust -//! use delaunay::prelude::triangulation::construction::{ +//! use delaunay::prelude::construction::{ //! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, //! }; -//! use delaunay::prelude::triangulation::insertion::InsertionError; +//! use delaunay::prelude::insertion::InsertionError; //! //! # fn main() -> Result<(), DelaunayTriangulationConstructionError> { //! let vertices = vec![ @@ -119,11 +131,11 @@ //! ### Topology guarantees and insertion-time validation (`TopologyGuarantee`, `ValidationPolicy`) //! //! ```rust -//! use delaunay::prelude::triangulation::construction::{ +//! use delaunay::prelude::construction::{ //! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyGuarantee, //! vertex, //! }; -//! use delaunay::prelude::triangulation::validation::ValidationPolicy; +//! use delaunay::prelude::validation::ValidationPolicy; //! //! # fn main() -> Result<(), DelaunayTriangulationConstructionError> { //! let vertices = vec![ @@ -149,10 +161,10 @@ //! ### Transactional operations and duplicate rejection //! //! ```rust -//! use delaunay::prelude::triangulation::construction::{ +//! use delaunay::prelude::construction::{ //! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, //! }; -//! use delaunay::prelude::triangulation::insertion::InsertionError; +//! use delaunay::prelude::insertion::InsertionError; //! //! # fn main() -> Result<(), DelaunayTriangulationConstructionError> { //! let vertices = vec![ @@ -204,22 +216,22 @@ //! These checks are surfaced via [`Tds::is_valid`](crate::tds::Tds::is_valid) //! (structural only) and [`Tds::validate`](crate::tds::Tds::validate) //! (Levels 1–2, elements + structural). For cumulative diagnostics across the full stack, -//! use [`DelaunayTriangulation::validation_report`](triangulation::delaunay::DelaunayTriangulation::validation_report). +//! use [`DelaunayTriangulation::validation_report`](crate::DelaunayTriangulation::validation_report). //! -//! - [`Triangulation`](crate::triangulation::Triangulation) builds on the TDS and validates +//! - [`Triangulation`] builds on the TDS and validates //! **manifold topology**. //! Level 3 (topology) validation is performed by -//! [`Triangulation::is_valid`](crate::triangulation::Triangulation::is_valid) (Level 3 only) and -//! [`Triangulation::validate`](crate::triangulation::Triangulation::validate) (Levels 1–3), which: +//! [`Triangulation::is_valid`](crate::Triangulation::is_valid) (Level 3 only) and +//! [`Triangulation::validate`](crate::Triangulation::validate) (Levels 1–3), which: //! - Strengthens facet sharing to the **manifold facet property**: each facet belongs to //! exactly 1 simplex (boundary) or exactly 2 simplices (interior). //! - Checks the **Euler characteristic** of the triangulation (using the topology module). //! -//! - [`DelaunayTriangulation`](crate::triangulation::delaunay::DelaunayTriangulation) builds on +//! - [`DelaunayTriangulation`] builds on //! `Triangulation` and validates the **geometric** Delaunay condition. //! Level 4 (Delaunay property) validation is performed by -//! [`DelaunayTriangulation::is_valid`](triangulation::delaunay::DelaunayTriangulation::is_valid) (Level 4 only) and -//! [`DelaunayTriangulation::validate`](triangulation::delaunay::DelaunayTriangulation::validate) (Levels 1–4). +//! [`DelaunayTriangulation::is_valid`](crate::DelaunayTriangulation::is_valid) (Level 4 only) and +//! [`DelaunayTriangulation::validate`](crate::DelaunayTriangulation::validate) (Levels 1–4). //! Construction is designed to satisfy the Delaunay property, but in rare cases it may be violated for //! near-degenerate inputs (see [Issue #120](https://github.com/acgetchell/delaunay/issues/120)). //! @@ -245,20 +257,20 @@ //! //! In addition to explicit validation calls, incremental construction (`new()` / `insert*()`) can run an //! automatic **Level 3** topology validation pass after insertion, controlled by -//! [`ValidationPolicy`](crate::prelude::triangulation::validation::ValidationPolicy). +//! [`ValidationPolicy`](crate::prelude::validation::ValidationPolicy). //! -//! The default is [`ValidationPolicy::OnSuspicion`](crate::prelude::triangulation::validation::ValidationPolicy::OnSuspicion): +//! The default is [`ValidationPolicy::OnSuspicion`](crate::prelude::validation::ValidationPolicy::OnSuspicion): //! Level 3 validation runs only when insertion takes a suspicious path (e.g. perturbation retries, //! repair loops, or neighbor-pointer repairs that actually changed pointers). //! //! This automatic pass only runs Level 3 (`Triangulation::is_valid()`). It does **not** run Level 4. //! //! ```rust -//! use delaunay::prelude::triangulation::construction::{ +//! use delaunay::prelude::construction::{ //! DelaunayTriangulation, DelaunayTriangulationConstructionError, vertex, //! }; -//! use delaunay::prelude::triangulation::insertion::InsertionError; -//! use delaunay::prelude::triangulation::validation::ValidationPolicy; +//! use delaunay::prelude::insertion::InsertionError; +//! use delaunay::prelude::validation::ValidationPolicy; //! //! # #[derive(Debug, thiserror::Error)] //! # enum ExampleError { @@ -294,25 +306,25 @@ //! definitions and rationale live in `docs/invariants.md`. //! //! Level 3 topology validation is parameterized by -//! [`TopologyGuarantee`](crate::prelude::triangulation::construction::TopologyGuarantee). This is separate from +//! [`TopologyGuarantee`](crate::prelude::construction::TopologyGuarantee). This is separate from //! `ValidationPolicy`: it controls *what* invariants Level 3 enforces, not *when* automatic //! validation runs. //! -//! - [`TopologyGuarantee::PLManifold`](crate::prelude::triangulation::construction::TopologyGuarantee::PLManifold) +//! - [`TopologyGuarantee::PLManifold`](crate::prelude::construction::TopologyGuarantee::PLManifold) //! (default): enforces manifold facet degree, boundary closure, connectedness, Euler characteristic, //! and link-based manifold conditions. Ridge-link checks are applied incrementally during insertion, //! with vertex-link validation performed at construction completion. //! //! The formal topological definitions, link conditions, and rationale for this validation strategy //! are documented in `docs/invariants.md`. -//! - [`TopologyGuarantee::PLManifoldStrict`](crate::prelude::triangulation::construction::TopologyGuarantee::PLManifoldStrict): +//! - [`TopologyGuarantee::PLManifoldStrict`](crate::prelude::construction::TopologyGuarantee::PLManifoldStrict): //! vertex-link validation after every insertion (slowest, maximum safety). -//! - [`TopologyGuarantee::Pseudomanifold`](crate::prelude::triangulation::construction::TopologyGuarantee::Pseudomanifold): +//! - [`TopologyGuarantee::Pseudomanifold`](crate::prelude::construction::TopologyGuarantee::Pseudomanifold): //! skips vertex-link validation (may be faster), but bistellar flip convergence is not guaranteed and //! you may want to validate the Delaunay property explicitly for near-degenerate inputs. //! //! ```rust -//! use delaunay::prelude::triangulation::construction::{ +//! use delaunay::prelude::construction::{ //! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, //! }; //! @@ -333,7 +345,7 @@ //! ``` //! //! ```rust -//! use delaunay::prelude::triangulation::construction::{ +//! use delaunay::prelude::construction::{ //! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, //! }; //! @@ -363,9 +375,9 @@ //! previous state. //! - **Duplicate detection**: Near-duplicate coordinates are rejected using a scale-aware //! Euclidean tolerance based on nearby geometry and floating-point resolution, returning -//! [`InsertionError::DuplicateCoordinates`](crate::prelude::triangulation::insertion::InsertionError::DuplicateCoordinates). +//! [`InsertionError::DuplicateCoordinates`](crate::prelude::insertion::InsertionError::DuplicateCoordinates). //! Duplicate UUIDs return -//! [`InsertionError::DuplicateUuid`](crate::prelude::triangulation::insertion::InsertionError::DuplicateUuid). +//! [`InsertionError::DuplicateUuid`](crate::prelude::insertion::InsertionError::DuplicateUuid). //! - **Explicit verification**: Use `dt.validate()` for cumulative verification (Levels 1–4), or //! `dt.is_valid()` for Level 4 only. @@ -382,7 +394,7 @@ /// [`Tds`](crate::tds::Tds), [`Simplex`](crate::tds::Simplex), /// [`FacetView`](crate::tds::FacetView), /// [`Vertex`](crate::tds::Vertex), the generic -/// [`Triangulation`](crate::triangulation::Triangulation) wrapper, and +/// [`Triangulation`] wrapper, and /// algorithm building blocks used by the crate. /// /// Public docs, examples, benchmarks, and downstream-style tests should prefer @@ -397,9 +409,8 @@ /// - [`crate::query`] / [`crate::prelude::query`] for read-only traversal, /// adjacency, convex hull, and set-comparison helpers. /// -/// High-level Delaunay construction and builder APIs live under -/// [`crate::triangulation`] and the focused Delaunay-facing preludes, not under -/// `core`. +/// High-level Delaunay construction and builder APIs live at the crate root +/// and under the focused Delaunay-facing preludes, not under `core`. #[expect( clippy::redundant_pub_crate, reason = "`pub(crate)` keeps internal cross-module intent visible while `core` is private" @@ -541,13 +552,25 @@ mod core { pub use secondary_maps::*; pub use triangulation_maps::*; } + /// Generic triangulation construction helpers. + pub mod construction; pub mod edge; pub mod facet; + /// Incremental insertion for generic triangulations. + pub mod insertion; /// Semantic classification and telemetry for topological operations pub mod operations; + /// Geometric orientation validation and canonicalization for generic triangulations. + pub mod orientation; + /// Read-only query and traversal helpers for generic triangulations. + pub mod query; + /// Local topology repair for generic triangulations. + pub mod repair; pub mod tds; /// Generic triangulation combining kernel + Tds. pub mod triangulation; + /// Generic validation orchestration for triangulations. + pub mod validation; /// General utility functions organized by functionality. pub mod util { @@ -877,7 +900,85 @@ pub mod geometry { pub use util::*; } -pub mod triangulation; +/// Fluent builder for Delaunay triangulations. +#[path = "delaunay/builder.rs"] +pub mod builder; +/// Batch construction options, errors, statistics, and policy helpers. +#[path = "delaunay/construction.rs"] +pub mod construction; +/// Read-only Delaunay query, traversal, and accessor methods. +#[path = "delaunay/query.rs"] +pub(crate) mod delaunay_query; +/// End-to-end "repair then delaunayize" workflow. +#[path = "delaunay/delaunayize.rs"] +pub mod delaunayize; +/// Construction and performance diagnostics. +#[path = "delaunay/diagnostics.rs"] +pub mod diagnostics; +/// Triangulation editing operations (bistellar flips). +#[path = "delaunay/flips.rs"] +pub mod flips; +/// Post-construction vertex insertion and removal operations. +#[path = "delaunay/insertion.rs"] +pub(crate) mod insertion; +#[path = "delaunay/locality.rs"] +pub(crate) mod locality; +/// Repair policies and outcomes for Delaunay triangulations. +#[path = "delaunay/repair.rs"] +pub mod repair; +/// Serialization support for Delaunay triangulations. +#[path = "delaunay/serialization.rs"] +pub(crate) mod serialization; +/// Delaunay triangulation layer with incremental insertion. +#[path = "delaunay/triangulation.rs"] +pub(crate) mod triangulation; +/// Validation scheduling helpers for triangulation diagnostics. +#[path = "delaunay/validation.rs"] +pub mod validation; + +// Re-export commonly used Delaunay-facing types at the crate root. +pub use crate::builder::DelaunayTriangulationBuilder; +pub use crate::construction::{ + ConstructionOptions, ConstructionSkipSample, ConstructionSlowInsertionSample, + ConstructionStatistics, DedupPolicy, DelaunayConstructionFailure, + DelaunayConstructionRepairPhase, DelaunayTriangulationConstructionError, + DelaunayTriangulationConstructionErrorWithStatistics, InitialSimplexStrategy, + InsertionOrderStrategy, RetryPolicy, +}; +pub use crate::core::algorithms::incremental_insertion::{ + CavityFillingError, CavityRepairStage, DelaunayRepairErrorKind, DelaunayRepairErrorSummary, + DelaunayRepairFailureContext, HullExtensionReason, InitialSimplexConstructionError, + InsertionError, InsertionErrorKind, InsertionErrorSourceKind, InsertionErrorSummary, + NeighborRebuildError, NeighborWiringError, TdsConstructionFailure, TdsValidationFailure, + extend_hull, fill_cavity, repair_neighbor_pointers, repair_neighbor_pointers_local, + wire_cavity_neighbors, +}; +pub use crate::core::algorithms::pl_manifold_repair::{ + PlManifoldRepairError, PlManifoldRepairStats, +}; +pub use crate::core::construction::TriangulationConstructionError; +pub use crate::core::insertion::DuplicateDetectionMetrics; +pub use crate::core::operations::{ + InsertionOutcome, InsertionResult, InsertionStatistics, RepairDecision, RepairSkipReason, + SuspicionFlags, TopologicalOperation, +}; +pub use crate::core::triangulation::Triangulation; +pub use crate::core::util::DeduplicationError; +pub use crate::core::util::{DelaunayValidationError, find_delaunay_violations}; +#[cfg(feature = "diagnostics")] +pub use crate::core::util::{ + DelaunayViolationDetail, DelaunayViolationReport, debug_print_first_delaunay_violation, + delaunay_violation_report, +}; +pub use crate::core::validation::{ + TopologyGuarantee, TriangulationValidationError, ValidationPolicy, +}; +pub use crate::repair::{ + DelaunayCheckPolicy, DelaunayRepairHeuristicConfig, DelaunayRepairHeuristicSeeds, + DelaunayRepairOperation, DelaunayRepairOutcome, DelaunayRepairPolicy, +}; +pub use crate::triangulation::*; +pub use crate::validation::DelaunayTriangulationValidationError; /// Topology analysis and validation for triangulated spaces. /// @@ -900,7 +1001,7 @@ pub mod triangulation; /// # Example /// /// ```rust -/// use delaunay::prelude::triangulation::construction::{ +/// use delaunay::prelude::construction::{ /// DelaunayTriangulation, DelaunayTriangulationConstructionError, vertex, /// }; /// use delaunay::prelude::topology::validation; @@ -964,7 +1065,7 @@ pub mod topology { } // Re-export commonly used types - pub use crate::triangulation::TopologyGuarantee; + pub use crate::TopologyGuarantee; pub use characteristics::*; pub use manifold::{ ManifoldError, validate_closed_boundary, validate_facet_degree, validate_ridge_links, @@ -1060,8 +1161,8 @@ pub mod tds { /// Public low-level algorithms that are useful outside full construction. /// /// This module currently exposes point-location and conflict-region building -/// blocks. Higher-level Delaunay construction, repair, and editing APIs remain -/// under [`triangulation`] and the matching focused preludes. +/// blocks. Higher-level Delaunay construction, repair, and editing APIs are +/// available at the crate root and through the matching focused preludes. /// /// # Examples /// @@ -1134,8 +1235,7 @@ pub mod query { AdjacencyIndex, AdjacencyIndexBuildError, EdgeKey, FacetView, Simplex, SimplexKey, Vertex, VertexKey, }; - pub use crate::triangulation::Triangulation; - pub use crate::triangulation::delaunay::DelaunayTriangulation; + pub use crate::{DelaunayTriangulation, Triangulation}; } /// A prelude module that re-exports commonly used types and macros. @@ -1147,8 +1247,19 @@ pub mod prelude { DataSerialize, DataType, }; pub use crate::tds::*; - pub use crate::triangulation::delaunay::*; - pub use crate::triangulation::*; + pub use crate::{ + ConstructionOptions, ConstructionSkipSample, ConstructionSlowInsertionSample, + ConstructionStatistics, DedupPolicy, DelaunayCheckPolicy, DelaunayConstructionFailure, + DelaunayConstructionRepairPhase, DelaunayRepairHeuristicConfig, + DelaunayRepairHeuristicSeeds, DelaunayRepairOperation, DelaunayRepairOutcome, + DelaunayRepairPolicy, DelaunayTriangulation, DelaunayTriangulationBuilder, + DelaunayTriangulationConstructionError, + DelaunayTriangulationConstructionErrorWithStatistics, DelaunayTriangulationValidationError, + DuplicateDetectionMetrics, InitialSimplexStrategy, InsertionOrderStrategy, InsertionResult, + PlManifoldRepairError, PlManifoldRepairStats, RepairDecision, RepairSkipReason, + RetryPolicy, TopologicalOperation, TopologyGuarantee, Triangulation, + TriangulationConstructionError, TriangulationValidationError, ValidationPolicy, + }; // Re-export utility items, but avoid exporting the util module names themselves. // @@ -1158,10 +1269,9 @@ pub mod prelude { HilbertError, hilbert_index, hilbert_indices_prequantized, hilbert_quantize, hilbert_sort_by_stable, hilbert_sort_by_unstable, hilbert_sorted_indices, }; - pub use self::triangulation::repair::{DelaunayValidationError, find_delaunay_violations}; - pub use self::triangulation::{ - DeduplicationError, dedup_vertices_epsilon, dedup_vertices_exact, - filter_vertices_excluding, try_dedup_vertices_epsilon, + pub use crate::core::util::{ + DeduplicationError, DelaunayValidationError, dedup_vertices_epsilon, dedup_vertices_exact, + filter_vertices_excluding, find_delaunay_violations, try_dedup_vertices_epsilon, }; pub use crate::query::{ JaccardComputationError, extract_edge_set, extract_facet_identifier_set, @@ -1181,16 +1291,16 @@ pub mod prelude { }; // Re-export incremental insertion types - pub use crate::triangulation::{ + pub use crate::{ CavityFillingError, CavityRepairStage, DelaunayRepairErrorKind, DelaunayRepairErrorSummary, DelaunayRepairFailureContext, HullExtensionReason, InitialSimplexConstructionError, InsertionError, InsertionErrorKind, InsertionErrorSourceKind, InsertionErrorSummary, NeighborRebuildError, NeighborWiringError, TdsConstructionFailure, TdsValidationFailure, }; - pub use crate::triangulation::{InsertionOutcome, InsertionStatistics, SuspicionFlags}; + pub use crate::{InsertionOutcome, InsertionStatistics, SuspicionFlags}; // Re-export diagnostic types for scientific analysis of construction and repair - pub use crate::triangulation::flips::{ + pub use crate::flips::{ DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairStats, DelaunayRepairVerificationContext, FlipContextError, FlipEdgeAdjacencyError, FlipError, FlipMutationError, FlipNeighborWiringError, FlipPredicateError, FlipPredicateOperation, @@ -1211,228 +1321,230 @@ pub mod prelude { robust_predicates::*, traits::coordinate::*, util::*, }; - /// Focused exports for triangulation construction and mutation. + /// Batch construction options, builders, and construction errors. + /// + /// This focused prelude is for callers configuring Delaunay construction + /// without importing the broader triangulation editing and repair + /// surface. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{ + /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, + /// }; + /// + /// # fn main() -> Result<(), DelaunayTriangulationConstructionError> { + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let triangulation = DelaunayTriangulationBuilder::new(&vertices) + /// .build::<()>()?; + /// + /// assert_eq!(triangulation.number_of_vertices(), 3); + /// # Ok(()) + /// # } + /// ``` + pub mod construction { + pub use crate::builder::{ + DelaunayTriangulationBuilder, ExplicitConstructionError, + ExplicitDelaunayValidationError, ExplicitDelaunayValidationErrorKind, + ExplicitDelaunayValidationSourceKind, ExplicitInsertionError, + ExplicitInsertionErrorKind, ExplicitInvariantError, ExplicitInvariantErrorKind, + ExplicitTdsError, ExplicitTdsErrorKind, + }; + pub use crate::construction::{ + ConstructionOptions, ConstructionSkipSample, ConstructionSlowInsertionSample, + ConstructionStatistics, DedupPolicy, DelaunayConstructionFailure, + DelaunayConstructionRepairPhase, DelaunayTriangulationConstructionError, + DelaunayTriangulationConstructionErrorWithStatistics, InitialSimplexStrategy, + InsertionOrderStrategy, RetryPolicy, + }; + pub use crate::repair::DelaunayRepairPolicy; + pub use crate::tds::{ + SimplexValidationError, Vertex, VertexBuilder, VertexBuilderError, + VertexValidationError, + }; + pub use crate::topology::traits::{GlobalTopology, TopologyKind, ToroidalConstructionMode}; + pub use crate::validation::DelaunayTriangulationValidationError; + pub use crate::{ + CavityFillingError, CavityRepairStage, DelaunayTriangulation, TopologyGuarantee, + Triangulation, TriangulationConstructionError, + }; + // Convenience macro (commonly used in docs/examples). + pub use crate::vertex; + } + + /// Generic triangulation construction, validation, query, and local repair. + /// + /// This focused prelude is for callers working directly with + /// [`Triangulation`] rather than the higher-level + /// [`DelaunayTriangulation`] wrapper. It keeps the generic TDS/kernel/error + /// types needed by public `Triangulation` methods together without pulling + /// in Delaunay repair, delaunayize, or batch-construction APIs. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::triangulation::{ + /// FastKernel, Triangulation, TriangulationConstructionError, vertex, + /// }; + /// + /// # fn main() -> Result<(), TriangulationConstructionError> { + /// let vertices = vec![ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let tds = Triangulation::, (), (), 2>::build_initial_simplex(&vertices)?; + /// + /// assert_eq!(tds.number_of_vertices(), 3); + /// assert_eq!(tds.number_of_simplices(), 1); + /// # Ok(()) + /// # } + /// ``` pub mod triangulation { - pub use crate::core::util::{ - DeduplicationError, dedup_vertices_epsilon, dedup_vertices_exact, - filter_vertices_excluding, try_dedup_vertices_epsilon, + pub use crate::collections::{FacetIssuesMap, SimplexKeyBuffer, SmallBuffer}; + pub use crate::geometry::kernel::{ + AdaptiveKernel, ExactPredicates, FastKernel, Kernel, RobustKernel, }; + pub use crate::geometry::point::Point; pub use crate::query::{ - DataCopy, DataDebug, DataDeserialize, DataIdentity, DataSerde, DataSerialize, DataType, + AdjacencyIndex, AdjacencyIndexBuildError, BoundaryAnalysis, DataCopy, DataDebug, + DataDeserialize, DataIdentity, DataSerde, DataSerialize, DataType, EdgeKey, FacetView, }; - pub use crate::tds::{Vertex, VertexBuilder, VertexBuilderError, VertexValidationError}; - pub use crate::topology::traits::{GlobalTopology, TopologyKind, ToroidalConstructionMode}; - pub use crate::triangulation::builder::*; - pub use crate::triangulation::delaunay::*; - pub use crate::triangulation::{ - DuplicateDetectionMetrics, TopologyGuarantee, Triangulation, - TriangulationConstructionError, TriangulationValidationError, ValidationPolicy, + pub use crate::tds::{ + FacetHandle, InvariantError, InvariantErrorSummary, InvariantErrorSummaryDetail, + InvariantErrorSummaryKind, NeighborSlot, Simplex, SimplexKey, Tds, + TdsConstructionError, TdsError, TdsErrorKind, TdsMutationError, + TriangulationValidationErrorKind, Vertex, VertexBuilder, VertexBuilderError, VertexKey, }; - pub use crate::triangulation::{ - InsertionOutcome, InsertionStatistics, RepairDecision, RepairSkipReason, - SuspicionFlags, TopologicalOperation, + pub use crate::{ + InsertionError, TopologyGuarantee, Triangulation, TriangulationConstructionError, + TriangulationValidationError, ValidationPolicy, }; - /// Batch construction options, builders, and construction errors. - /// - /// This focused prelude is for callers configuring Delaunay construction - /// without importing the broader triangulation editing and repair - /// surface. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::construction::{ - /// DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, - /// }; - /// - /// # fn main() -> Result<(), DelaunayTriangulationConstructionError> { - /// let vertices = vec![ - /// vertex!([0.0, 0.0]), - /// vertex!([1.0, 0.0]), - /// vertex!([0.0, 1.0]), - /// ]; - /// let triangulation = DelaunayTriangulationBuilder::new(&vertices) - /// .build::<()>()?; - /// - /// assert_eq!(triangulation.number_of_vertices(), 3); - /// # Ok(()) - /// # } - /// ``` - pub mod construction { - pub use crate::tds::{ - SimplexValidationError, Vertex, VertexBuilder, VertexBuilderError, - VertexValidationError, - }; - pub use crate::topology::traits::{ - GlobalTopology, TopologyKind, ToroidalConstructionMode, - }; - pub use crate::triangulation::builder::{ - DelaunayTriangulationBuilder, ExplicitConstructionError, - ExplicitDelaunayValidationError, ExplicitDelaunayValidationErrorKind, - ExplicitDelaunayValidationSourceKind, ExplicitInsertionError, - ExplicitInsertionErrorKind, ExplicitInvariantError, ExplicitInvariantErrorKind, - ExplicitTdsError, ExplicitTdsErrorKind, - }; - pub use crate::triangulation::delaunay::{ - ConstructionOptions, ConstructionSkipSample, ConstructionSlowInsertionSample, - ConstructionStatistics, DedupPolicy, DelaunayConstructionFailure, - DelaunayConstructionRepairPhase, DelaunayRepairPolicy, DelaunayTriangulation, - DelaunayTriangulationConstructionError, - DelaunayTriangulationConstructionErrorWithStatistics, InitialSimplexStrategy, - InsertionOrderStrategy, RetryPolicy, - }; - pub use crate::triangulation::{ - CavityFillingError, CavityRepairStage, TopologyGuarantee, Triangulation, - TriangulationConstructionError, - }; - // Convenience macro (commonly used in docs/examples). - pub use crate::vertex; - } - - /// Bistellar (Pachner) flips for explicit triangulation editing. - /// - /// Repair-only diagnostics and validation helpers are intentionally - /// excluded; use [`crate::prelude::triangulation::repair`] for those. - /// - /// ```compile_fail - /// use delaunay::prelude::triangulation::flips::DelaunayRepairError; - /// ``` - /// - pub mod flips { - pub use crate::collections::{ - MAX_PRACTICAL_DIMENSION_SIZE, SimplexKeyBuffer, SmallBuffer, - }; - pub use crate::tds::{EdgeKey, FacetHandle, SimplexKey, VertexKey}; - pub use crate::triangulation::delaunay::DelaunayTriangulation; - pub use crate::triangulation::flips::{ - BistellarFlipKind, BistellarFlips, FlipContextError, FlipDirection, - FlipEdgeAdjacencyError, FlipError, FlipInfo, FlipMutationError, - FlipNeighborWiringError, FlipPredicateError, FlipPredicateOperation, - FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RidgeHandle, TriangleHandle, - }; - pub use crate::triangulation::flips::{BistellarMove, ConstK}; - - // Convenience macro (commonly used in docs/examples). - pub use crate::vertex; - } + // Convenience macro for generic triangulation examples and tests. + pub use crate::vertex; + } - /// Incremental insertion building blocks and diagnostics. - /// - /// Includes compact [`InsertionErrorSummary`] and [`InsertionErrorKind`] - /// exports for callers that need small by-value diagnostics instead of full insertion - /// error payloads. - /// - /// [`InsertionErrorSummary`]: crate::prelude::triangulation::insertion::InsertionErrorSummary - /// [`InsertionErrorKind`]: crate::prelude::triangulation::insertion::InsertionErrorKind - pub mod insertion { - pub use crate::collections::SimplexKeyBuffer; - pub use crate::tds::FacetHandle; - pub use crate::tds::{SimplexKey, Tds, TdsMutationError, VertexKey}; - pub use crate::triangulation::{ - CavityFillingError, CavityRepairStage, DelaunayRepairErrorKind, - DelaunayRepairErrorSummary, DelaunayRepairFailureContext, HullExtensionReason, - InitialSimplexConstructionError, InsertionError, InsertionErrorKind, - InsertionErrorSourceKind, InsertionErrorSummary, NeighborRebuildError, - NeighborWiringError, TdsConstructionFailure, TdsValidationFailure, extend_hull, - fill_cavity, repair_neighbor_pointers, repair_neighbor_pointers_local, - wire_cavity_neighbors, - }; - pub use crate::triangulation::{ - InsertionOutcome, InsertionResult, InsertionStatistics, - }; - } + /// Bistellar (Pachner) flips for explicit triangulation editing. + /// + /// Repair-only diagnostics and validation helpers are intentionally + /// excluded; use [`crate::prelude::repair`] for those. + /// + /// ```compile_fail + /// use delaunay::prelude::flips::DelaunayRepairError; + /// ``` + /// + pub mod flips { + pub use crate::DelaunayTriangulation; + pub use crate::collections::{MAX_PRACTICAL_DIMENSION_SIZE, SimplexKeyBuffer, SmallBuffer}; + pub use crate::flips::{ + BistellarFlipKind, BistellarFlips, FlipContextError, FlipDirection, + FlipEdgeAdjacencyError, FlipError, FlipInfo, FlipMutationError, + FlipNeighborWiringError, FlipPredicateError, FlipPredicateOperation, + FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RidgeHandle, TriangleHandle, + }; + pub use crate::flips::{BistellarMove, ConstK}; + pub use crate::tds::{EdgeKey, FacetHandle, SimplexKey, VertexKey}; - /// Topological operation telemetry and repair decisions. - pub mod operations { - pub use crate::triangulation::{ - InsertionOutcome, InsertionResult, InsertionStatistics, RepairDecision, - RepairSkipReason, SuspicionFlags, TopologicalOperation, - }; - } + // Convenience macro (commonly used in docs/examples). + pub use crate::vertex; + } - /// Flip-based Delaunay repair, diagnostics, and Level 4 validation. - /// - /// Includes compact [`DelaunayRepairErrorSummary`] and [`DelaunayRepairErrorKind`] - /// exports for APIs that need repair categories without retaining full repair - /// diagnostics. - /// - /// [`DelaunayRepairErrorSummary`]: crate::prelude::triangulation::repair::DelaunayRepairErrorSummary - /// [`DelaunayRepairErrorKind`]: crate::prelude::triangulation::repair::DelaunayRepairErrorKind - pub mod repair { - pub use crate::triangulation::delaunay::{ - DelaunayCheckPolicy, DelaunayRepairHeuristicConfig, DelaunayRepairHeuristicSeeds, - DelaunayRepairOperation, DelaunayRepairOutcome, DelaunayRepairPolicy, - DelaunayTriangulation, DelaunayTriangulationValidationError, - }; - pub use crate::triangulation::flips::{ - DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairStats, - DelaunayRepairVerificationContext, FlipContextError, FlipEdgeAdjacencyError, - FlipError, FlipMutationError, FlipNeighborWiringError, FlipPredicateError, - FlipPredicateOperation, FlipTriangleAdjacencyError, FlipVertexAdjacencyError, - RepairQueueOrder, verify_delaunay_for_triangulation, - verify_delaunay_via_flip_predicates, - }; - pub use crate::triangulation::{DelaunayRepairErrorKind, DelaunayRepairErrorSummary}; - pub use crate::triangulation::{DelaunayValidationError, find_delaunay_violations}; - pub use crate::triangulation::{TopologyGuarantee, Triangulation, ValidationPolicy}; - } + /// Incremental insertion building blocks and diagnostics. + /// + /// Includes compact [`InsertionErrorSummary`] and [`InsertionErrorKind`] + /// exports for callers that need small by-value diagnostics instead of full insertion + /// error payloads. + /// + /// [`InsertionErrorSummary`]: crate::prelude::insertion::InsertionErrorSummary + /// [`InsertionErrorKind`]: crate::prelude::insertion::InsertionErrorKind + 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, + DelaunayRepairErrorSummary, DelaunayRepairFailureContext, HullExtensionReason, + InitialSimplexConstructionError, InsertionError, InsertionErrorKind, + InsertionErrorSourceKind, InsertionErrorSummary, NeighborRebuildError, + NeighborWiringError, TdsConstructionFailure, TdsValidationFailure, extend_hull, + fill_cavity, repair_neighbor_pointers, repair_neighbor_pointers_local, + wire_cavity_neighbors, + }; + pub use crate::{InsertionOutcome, InsertionResult, InsertionStatistics}; + } - /// End-to-end "repair then delaunayize" workflow. - /// - /// Self-contained: a single `use delaunay::prelude::triangulation::delaunayize::*` - /// import brings in [`DelaunayTriangulation`], [`vertex!`], and all - /// delaunayize-specific types. - pub mod delaunayize { - pub use crate::triangulation::delaunay::DelaunayTriangulation; - pub use crate::triangulation::delaunayize::*; - pub use crate::triangulation::{PlManifoldRepairError, PlManifoldRepairStats}; - - // Convenience macro (commonly used in docs/examples). - pub use crate::vertex; - } + /// Topological operation telemetry and repair decisions. + pub mod operations { + pub use crate::{ + InsertionOutcome, InsertionResult, InsertionStatistics, RepairDecision, + RepairSkipReason, SuspicionFlags, TopologicalOperation, + }; + } - /// Construction telemetry diagnostics. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::diagnostics::ConstructionTelemetry; - /// - /// let telemetry = ConstructionTelemetry::default(); - /// assert!(!telemetry.has_data()); - /// ``` - pub mod diagnostics { - pub use crate::triangulation::diagnostics::{ - BatchLocalRepairTrigger, ConstructionTelemetry, LocalRepairSample, - }; - } + /// Flip-based Delaunay repair, diagnostics, and Level 4 validation. + /// + /// Includes compact [`DelaunayRepairErrorSummary`] and [`DelaunayRepairErrorKind`] + /// exports for APIs that need repair categories without retaining full repair + /// diagnostics. + /// + /// [`DelaunayRepairErrorSummary`]: crate::prelude::repair::DelaunayRepairErrorSummary + /// [`DelaunayRepairErrorKind`]: crate::prelude::repair::DelaunayRepairErrorKind + pub mod repair { + pub use crate::flips::{ + DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairStats, + DelaunayRepairVerificationContext, FlipContextError, FlipEdgeAdjacencyError, FlipError, + FlipMutationError, FlipNeighborWiringError, FlipPredicateError, FlipPredicateOperation, + FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RepairQueueOrder, + verify_delaunay_for_triangulation, verify_delaunay_via_flip_predicates, + }; + pub use crate::repair::{ + DelaunayCheckPolicy, DelaunayRepairHeuristicConfig, DelaunayRepairHeuristicSeeds, + DelaunayRepairOutcome, DelaunayRepairPolicy, + }; + pub use crate::{ + DelaunayRepairErrorKind, DelaunayRepairErrorSummary, DelaunayRepairOperation, + DelaunayTriangulation, DelaunayTriangulationValidationError, + }; + pub use crate::{DelaunayValidationError, find_delaunay_violations}; + pub use crate::{TopologyGuarantee, Triangulation, ValidationPolicy}; + } - /// Validation scheduling helpers for construction diagnostics. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::validation::ValidationCadence; - /// - /// let cadence = ValidationCadence::from_optional_every(Some(32)); - /// assert!(!cadence.should_validate(31)); - /// assert!(cadence.should_validate(32)); - /// ``` - pub mod validation { - pub use crate::triangulation::delaunay::DelaunayTriangulationValidationError; - pub use crate::triangulation::validation::*; - pub use crate::triangulation::{TriangulationValidationError, ValidationPolicy}; - } + /// End-to-end "repair then delaunayize" workflow. + /// + /// Self-contained: a single `use delaunay::prelude::delaunayize::*` + /// import brings in [`DelaunayTriangulation`], [`vertex!`], and all + /// delaunayize-specific types. + pub mod delaunayize { + pub use crate::DelaunayTriangulation; + pub use crate::delaunayize::*; + pub use crate::{PlManifoldRepairError, PlManifoldRepairStats}; + + // Convenience macro (commonly used in docs/examples). + pub use crate::vertex; + } - pub use crate::triangulation::{ - CavityFillingError, CavityRepairStage, DelaunayRepairErrorKind, - DelaunayRepairErrorSummary, DelaunayRepairFailureContext, HullExtensionReason, - InsertionError, InsertionErrorKind, InsertionErrorSourceKind, InsertionErrorSummary, - NeighborWiringError, + /// Validation scheduling helpers for construction diagnostics. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::validation::ValidationCadence; + /// + /// let cadence = ValidationCadence::from_optional_every(Some(32)); + /// assert!(!cadence.should_validate(31)); + /// assert!(cadence.should_validate(32)); + /// ``` + pub mod validation { + pub use crate::validation::*; + pub use crate::{ + DelaunayTriangulationValidationError, TriangulationValidationError, ValidationPolicy, }; - // Convenience macro (commonly used in docs/tests/examples). - pub use crate::vertex; } /// Focused exports for collection types used throughout the crate. @@ -1552,10 +1664,11 @@ pub mod prelude { }; } - /// Focused exports for opt-in diagnostic helpers. + /// Focused exports for construction telemetry and opt-in diagnostic helpers. /// - /// These helpers are compiled only with the `diagnostics` feature because - /// they are intended for explicit debugging and verification workflows, not + /// Construction telemetry is always available. Expensive verification and + /// violation-report helpers are compiled only with the `diagnostics` + /// feature because they are intended for explicit debugging workflows, not /// the default public API surface. /// /// # Examples @@ -1565,12 +1678,17 @@ pub mod prelude { /// /// assert!(NeighborSlot::Boundary.is_boundary()); /// ``` - #[cfg(feature = "diagnostics")] - #[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))] pub mod diagnostics { + #[cfg(feature = "diagnostics")] + #[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))] pub use crate::algorithms::verify_conflict_region_completeness; + pub use crate::diagnostics::{ + BatchLocalRepairTrigger, ConstructionTelemetry, LocalRepairSample, + }; pub use crate::tds::NeighborSlot; - pub use crate::triangulation::{ + #[cfg(feature = "diagnostics")] + #[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))] + pub use crate::{ DelaunayViolationDetail, DelaunayViolationReport, debug_print_first_delaunay_violation, delaunay_violation_report, }; @@ -1611,8 +1729,7 @@ pub mod prelude { pub use crate::tds::{ AdjacencyIndex, AdjacencyIndexBuildError, EdgeKey, SimplexKey, VertexKey, }; - pub use crate::triangulation::Triangulation; - pub use crate::triangulation::delaunay::DelaunayTriangulation; + pub use crate::{DelaunayTriangulation, Triangulation}; // Common input/output types (kept intentionally small) pub use crate::geometry::Point; @@ -1669,6 +1786,8 @@ pub mod prelude { /// # } /// ``` pub mod generators { + pub use crate::TopologyGuarantee; + pub use crate::construction::InsertionOrderStrategy; pub use crate::geometry::util::{ RandomPointGenerationError, RandomTriangulationBuilder, generate_grid_points, generate_poisson_points, generate_random_points, generate_random_points_in_ball, @@ -1676,8 +1795,6 @@ pub mod prelude { generate_random_points_seeded, generate_random_triangulation, generate_random_triangulation_with_topology_guarantee, scaled_bounds_by_point_count, }; - pub use crate::triangulation::TopologyGuarantee; - pub use crate::triangulation::delaunay::InsertionOrderStrategy; } /// Focused exports for Hilbert ordering and quantization utilities. @@ -1747,6 +1864,7 @@ pub const fn is_normal() -> bool { #[cfg(test)] mod tests { use crate::{ + DelaunayTriangulation, core::{ adjacency::AdjacencyIndex, edge::EdgeKey, simplex::Simplex, tds::Tds, triangulation::Triangulation, vertex::Vertex, @@ -1755,19 +1873,18 @@ mod tests { Point, algorithms::convex_hull::ConvexHull, kernel::AdaptiveKernel, kernel::FastKernel, }, is_normal, - prelude::triangulation::delaunayize::{ + prelude::delaunayize::{ DelaunayTriangulationConstructionError, DelaunayizeConfig, DelaunayizeError, DelaunayizeOutcome, PlManifoldRepairError, PlManifoldRepairStats, SimplexValidationError, }, - prelude::triangulation::repair::{ + prelude::repair::{ DelaunayCheckPolicy, DelaunayRepairError, DelaunayRepairOutcome, DelaunayRepairPolicy, DelaunayRepairStats, DelaunayTriangulation as RepairDelaunayTriangulation, FlipContextError, FlipError, RepairQueueOrder, TopologyGuarantee, verify_delaunay_for_triangulation, verify_delaunay_via_flip_predicates, }, prelude::*, - triangulation::delaunay::DelaunayTriangulation, vertex, }; diff --git a/src/topology/manifold.rs b/src/topology/manifold.rs index 8da1bd3f..46c712ae 100644 --- a/src/topology/manifold.rs +++ b/src/topology/manifold.rs @@ -296,7 +296,7 @@ pub enum ManifoldError { /// /// ```rust /// use delaunay::prelude::geometry::*; -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// use delaunay::prelude::topology::validation::validate_facet_degree; /// /// let vertices = vec![ @@ -351,7 +351,7 @@ pub fn validate_facet_degree( /// /// ```rust /// use delaunay::prelude::geometry::*; -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// use delaunay::prelude::topology::validation::validate_closed_boundary; /// /// let vertices = vec![ @@ -1266,7 +1266,7 @@ fn periodic_aware_ridge_star( /// /// ```rust /// use delaunay::prelude::geometry::*; -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// use delaunay::prelude::topology::validation::validate_ridge_links; /// /// let vertices = vec![ @@ -1341,7 +1341,7 @@ pub fn validate_ridge_links( /// # Examples /// ```rust /// use delaunay::prelude::geometry::*; -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// use delaunay::prelude::topology::validation::validate_ridge_links_for_simplices; /// use delaunay::prelude::collections::SimplexKeyBuffer; /// @@ -1428,7 +1428,7 @@ pub fn validate_ridge_links_for_simplices( /// /// ```rust /// use delaunay::prelude::geometry::*; -/// use delaunay::prelude::triangulation::*; +/// use delaunay::prelude::*; /// use delaunay::prelude::topology::validation::{ /// validate_closed_boundary, validate_facet_degree, validate_vertex_links, /// }; diff --git a/src/triangulation.rs b/src/triangulation.rs deleted file mode 100644 index 43a3bea6..00000000 --- a/src/triangulation.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Triangulation-facing APIs. -//! -//! This module is the public facade for triangulation workflows. It deliberately -//! stays thin: -//! -//! - [`crate::prelude::triangulation::Triangulation`] owns the generic -//! triangulation container and low-level mutation invariants. -//! - [`crate::triangulation`] owns higher-level construction, Delaunay repair, -//! diagnostics, validation scheduling, editing, and builder workflows. -//! - Submodules under this namespace keep those concerns separate while this -//! facade preserves the stable public import surface. -//! -//! # Examples -//! -//! ```rust -//! use delaunay::prelude::triangulation::construction::{ -//! DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, vertex, -//! }; -//! -//! # fn main() -> Result<(), DelaunayTriangulationConstructionError> { -//! let vertices = vec![ -//! vertex!([0.0, 0.0]), -//! vertex!([1.0, 0.0]), -//! vertex!([0.0, 1.0]), -//! ]; -//! let triangulation = DelaunayTriangulationBuilder::new(&vertices) -//! .build::<()>()?; -//! -//! assert_eq!(triangulation.number_of_vertices(), 3); -//! # Ok(()) -//! # } -//! ``` - -#![forbid(unsafe_code)] - -/// Fluent builder for Delaunay triangulations. -/// -/// See [`DelaunayTriangulation`](crate::triangulation::delaunay::DelaunayTriangulation) -/// for the constructed triangulation type. -pub mod builder; -/// Delaunay triangulation layer with incremental insertion. -pub mod delaunay; -/// End-to-end "repair then delaunayize" workflow. -pub mod delaunayize; -/// Construction and performance diagnostics. -pub mod diagnostics; -/// Triangulation editing operations (bistellar flips). -pub mod flips; -pub(crate) mod locality; -/// Validation scheduling helpers for triangulation diagnostics. -pub mod validation; - -// Re-export commonly used triangulation types for discoverability. -pub use crate::core::algorithms::incremental_insertion::{ - CavityFillingError, CavityRepairStage, DelaunayRepairErrorKind, DelaunayRepairErrorSummary, - DelaunayRepairFailureContext, HullExtensionReason, InitialSimplexConstructionError, - InsertionError, InsertionErrorKind, InsertionErrorSourceKind, InsertionErrorSummary, - NeighborRebuildError, NeighborWiringError, TdsConstructionFailure, TdsValidationFailure, - extend_hull, fill_cavity, repair_neighbor_pointers, repair_neighbor_pointers_local, - wire_cavity_neighbors, -}; -pub use crate::core::algorithms::pl_manifold_repair::{ - PlManifoldRepairError, PlManifoldRepairStats, -}; -pub use crate::core::operations::{ - InsertionOutcome, InsertionResult, InsertionStatistics, RepairDecision, RepairSkipReason, - SuspicionFlags, TopologicalOperation, -}; -pub use crate::core::triangulation::{ - DuplicateDetectionMetrics, TopologyGuarantee, Triangulation, TriangulationConstructionError, - TriangulationValidationError, ValidationPolicy, -}; -pub use crate::core::util::DeduplicationError; -pub use crate::core::util::{DelaunayValidationError, find_delaunay_violations}; -#[cfg(feature = "diagnostics")] -pub use crate::core::util::{ - DelaunayViolationDetail, DelaunayViolationReport, debug_print_first_delaunay_violation, - delaunay_violation_report, -}; -pub use crate::triangulation::builder::DelaunayTriangulationBuilder; -pub use crate::triangulation::delaunay::DelaunayTriangulation; diff --git a/src/triangulation/validation.rs b/src/triangulation/validation.rs deleted file mode 100644 index 0da759ec..00000000 --- a/src/triangulation/validation.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! Validation scheduling helpers for triangulation construction diagnostics. -//! -//! This module contains validation-control concepts that are orthogonal to the -//! Delaunay data structure itself. Keeping them here leaves -//! [`crate::triangulation::delaunay`] focused on construction, repair, and query logic. - -#![forbid(unsafe_code)] - -use std::num::NonZeroUsize; - -/// Cadence for explicit validation checkpoints during construction diagnostics. -/// -/// This is separate from [`ValidationPolicy`](crate::triangulation::ValidationPolicy), -/// which controls automatic insertion-time validation inside -/// [`Triangulation`](crate::triangulation::Triangulation). Diagnostic -/// harnesses can use this cadence for explicit periodic -/// [`DelaunayTriangulation::is_valid`](crate::triangulation::delaunay::DelaunayTriangulation::is_valid) -/// checks without overloading repair policy or exposing raw `Option` -/// scheduling in logs. -/// -/// # Examples -/// -/// ```rust -/// use delaunay::prelude::triangulation::validation::ValidationCadence; -/// -/// let cadence = ValidationCadence::from_optional_every(Some(128)); -/// assert!(!cadence.should_validate(0)); -/// assert!(!cadence.should_validate(127)); -/// assert!(cadence.should_validate(128)); -/// ``` -#[must_use = "validation cadence values only affect diagnostics when they are used"] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ValidationCadence { - /// Disable explicit periodic validation checkpoints. - Never, - /// Run explicit validation every N successful insertion attempts. - EveryN(NonZeroUsize), -} - -impl ValidationCadence { - /// Converts an optional integer cadence into a typed validation cadence. - /// - /// `None` and `Some(0)` disable periodic validation. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::validation::ValidationCadence; - /// - /// assert!(matches!( - /// ValidationCadence::from_optional_every(Some(32)), - /// ValidationCadence::EveryN(every) if every.get() == 32, - /// )); - /// assert_eq!( - /// ValidationCadence::from_optional_every(None), - /// ValidationCadence::Never, - /// ); - /// ``` - pub const fn from_optional_every(validate_every: Option) -> Self { - match validate_every { - None | Some(0) => Self::Never, - Some(every) => { - if let Some(every) = NonZeroUsize::new(every) { - Self::EveryN(every) - } else { - Self::Never - } - } - } - } - - /// Returns true when validation should run for a one-based insertion count. - /// - /// A count of `0` never triggers validation because no insertion has - /// completed yet. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::triangulation::validation::ValidationCadence; - /// - /// let cadence = ValidationCadence::from_optional_every(Some(4)); - /// assert!(!cadence.should_validate(0)); - /// assert!(!cadence.should_validate(3)); - /// assert!(cadence.should_validate(4)); - /// ``` - #[must_use] - pub const fn should_validate(self, insertion_count: usize) -> bool { - match self { - Self::Never => false, - Self::EveryN(every) => { - insertion_count != 0 && insertion_count.is_multiple_of(every.get()) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn validation_cadence_maps_optional_every() { - assert_eq!( - ValidationCadence::from_optional_every(None), - ValidationCadence::Never - ); - assert_eq!( - ValidationCadence::from_optional_every(Some(0)), - ValidationCadence::Never - ); - assert_eq!( - ValidationCadence::from_optional_every(Some(128)), - ValidationCadence::EveryN(NonZeroUsize::new(128).unwrap()) - ); - } - - #[test] - fn validation_cadence_should_validate_on_multiples() { - let cadence = ValidationCadence::EveryN(NonZeroUsize::new(64).unwrap()); - - assert!(!cadence.should_validate(0)); - assert!(!cadence.should_validate(63)); - assert!(cadence.should_validate(64)); - assert!(!cadence.should_validate(65)); - assert!(cadence.should_validate(128)); - assert!(!ValidationCadence::Never.should_validate(64)); - } -} diff --git a/tests/README.md b/tests/README.md index a5c26367..19e792e7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -351,7 +351,7 @@ Integration tests for serialization ensuring vertex identifiers and associated d #### [`delaunayize_workflow.rs`](./delaunayize_workflow.rs) -Integration tests for the `delaunayize_by_flips` workflow validating the public API in `delaunay::triangulation::delaunayize`. +Integration tests for the `delaunayize_by_flips` workflow validating the public API in `delaunay::delaunayize`. **Test Coverage:** @@ -374,7 +374,7 @@ issues #340, #341, and #342. or one of the active large-scale helpers: - `just debug-large-scale-2d [n] [repair_every]` — default `n=36000` -- `just debug-large-scale-3d [n] [repair_every]` — issue #341, default `n=8000` +- `just debug-large-scale-3d [n] [repair_every]` — issue #341, default `n=7500` - `just debug-large-scale-4d [n] [repair_every]` — issue #340, default `n=900` - `just debug-large-scale-5d [n] [repair_every]` — issue #342, default `n=140` diff --git a/tests/allocation_api.rs b/tests/allocation_api.rs index 41a85a5e..2ce80ae9 100644 --- a/tests/allocation_api.rs +++ b/tests/allocation_api.rs @@ -7,14 +7,14 @@ use allocation_counter::AllocationInfo; use delaunay::prelude::algorithms::{LocateError, LocateResult, locate_with_stats}; +use delaunay::prelude::construction::{ + DelaunayTriangulation, DelaunayTriangulationConstructionError, vertex, +}; use delaunay::prelude::geometry::{Coordinate, FastKernel, Point}; use delaunay::prelude::tds::{ SimplexKey, SimplexValidationError, TdsError, VertexKey, facet_key_from_vertices, measure_with_result, }; -use delaunay::prelude::triangulation::construction::{ - DelaunayTriangulation, DelaunayTriangulationConstructionError, vertex, -}; use std::hint::black_box; use thiserror::Error; diff --git a/tests/circumsphere_debug_tools.rs b/tests/circumsphere_debug_tools.rs index d50ab869..e866185a 100644 --- a/tests/circumsphere_debug_tools.rs +++ b/tests/circumsphere_debug_tools.rs @@ -15,8 +15,8 @@ use delaunay::geometry::matrix::{Matrix, determinant}; use delaunay::geometry::util::hypot; +use delaunay::prelude::construction::{Vertex, vertex}; use delaunay::prelude::geometry::*; -use delaunay::prelude::triangulation::construction::{Vertex, vertex}; use serde::{Deserialize, Serialize}; // Macro for standard test output formatting diff --git a/tests/dedup_batch_construction.rs b/tests/dedup_batch_construction.rs index 3d433770..966db4bb 100644 --- a/tests/dedup_batch_construction.rs +++ b/tests/dedup_batch_construction.rs @@ -9,7 +9,7 @@ //! //! Dimension coverage: 2D–5D via `gen_dedup_batch_tests!`. -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ ConstructionOptions, DedupPolicy, DelaunayConstructionFailure, DelaunayTriangulation, DelaunayTriangulationConstructionError, InsertionOrderStrategy, TopologyGuarantee, Vertex, vertex, diff --git a/tests/delaunay_edge_cases.rs b/tests/delaunay_edge_cases.rs index 9c03f226..5fb3acef 100644 --- a/tests/delaunay_edge_cases.rs +++ b/tests/delaunay_edge_cases.rs @@ -8,14 +8,14 @@ //! //! Converted from legacy `Tds::new()` tests to use the new `DelaunayTriangulation` API. +use delaunay::prelude::construction::{ + DelaunayConstructionFailure, DelaunayTriangulation, DelaunayTriangulationConstructionError, + TopologyGuarantee, Vertex, 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::RobustKernel; -use delaunay::prelude::triangulation::construction::{ - DelaunayConstructionFailure, DelaunayTriangulation, DelaunayTriangulationConstructionError, - TopologyGuarantee, Vertex, vertex, -}; use rand::SeedableRng; use rand::seq::SliceRandom; fn init_tracing() { diff --git a/tests/delaunay_incremental_insertion.rs b/tests/delaunay_incremental_insertion.rs index 69d26022..d298c2f2 100644 --- a/tests/delaunay_incremental_insertion.rs +++ b/tests/delaunay_incremental_insertion.rs @@ -13,13 +13,13 @@ use approx::assert_relative_eq; use delaunay::geometry::kernel::RobustKernel; use delaunay::prelude::algorithms::{LocateResult, find_conflict_region, locate}; use delaunay::prelude::collections::MAX_PRACTICAL_DIMENSION_SIZE; +use delaunay::prelude::construction::{ + ConstructionOptions, DedupPolicy, DelaunayTriangulation, TopologyGuarantee, Vertex, vertex, +}; use delaunay::prelude::geometry::{AdaptiveKernel, Coordinate, Point}; use delaunay::prelude::tds::{ Simplex, SimplexKey, SmallBuffer, VertexKey, facet_key_from_vertices, }; -use delaunay::prelude::triangulation::construction::{ - ConstructionOptions, DedupPolicy, DelaunayTriangulation, TopologyGuarantee, Vertex, vertex, -}; use uuid::Uuid; /// Build the canonical facet key used to compare neighbor mirror facets in tests. diff --git a/tests/delaunay_repair_fallback.rs b/tests/delaunay_repair_fallback.rs index 41af56e6..6f447ee5 100644 --- a/tests/delaunay_repair_fallback.rs +++ b/tests/delaunay_repair_fallback.rs @@ -4,12 +4,12 @@ //! Delaunay violations, the deterministic rebuild heuristic is triggered and //! successfully produces a valid Delaunay triangulation. -use delaunay::prelude::triangulation::construction::{ +use delaunay::flips::FacetHandle; +use delaunay::prelude::construction::{ DelaunayRepairPolicy, DelaunayTriangulation, TopologyGuarantee, vertex, }; -use delaunay::prelude::triangulation::flips::BistellarFlips; -use delaunay::prelude::triangulation::repair::DelaunayRepairHeuristicConfig; -use delaunay::triangulation::flips::FacetHandle; +use delaunay::prelude::flips::BistellarFlips; +use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; #[cfg(feature = "diagnostics")] fn init_tracing() { diff --git a/tests/delaunayize_workflow.rs b/tests/delaunayize_workflow.rs index ec6e5d5e..1100797c 100644 --- a/tests/delaunayize_workflow.rs +++ b/tests/delaunayize_workflow.rs @@ -1,6 +1,6 @@ //! Integration tests for the delaunayize-by-flips workflow. //! -//! Validates the public API in `delaunay::triangulation::delaunayize`, covering: +//! Validates the public API in `delaunay::delaunayize`, covering: //! - Non-Delaunay but PL-manifold success case //! - Config defaults //! - Outcome population on success and failure paths @@ -8,10 +8,10 @@ //! - Repeat-run determinism for outcome stats //! - Multi-dimensional coverage (2D–3D) -use delaunay::prelude::triangulation::construction::TriangulationConstructionError; -use delaunay::prelude::triangulation::delaunayize::*; -use delaunay::prelude::triangulation::flips::BistellarFlips; -use delaunay::triangulation::flips::FacetHandle; +use delaunay::flips::FacetHandle; +use delaunay::prelude::construction::TriangulationConstructionError; +use delaunay::prelude::delaunayize::*; +use delaunay::prelude::flips::BistellarFlips; use std::{error::Error, mem::size_of}; // ============================================================================= diff --git a/tests/euler_characteristic.rs b/tests/euler_characteristic.rs index b23d2ba4..45ec3cbd 100644 --- a/tests/euler_characteristic.rs +++ b/tests/euler_characteristic.rs @@ -14,18 +14,18 @@ //! //! For property-based tests with random triangulations, see `proptest_euler_characteristic.rs`. -use delaunay::prelude::geometry::AdaptiveKernel; -use delaunay::prelude::query::BoundaryAnalysis; -use delaunay::prelude::tds::Tds; -use delaunay::prelude::triangulation::construction::{ +use delaunay::builder::DelaunayTriangulationBuilder; +use delaunay::prelude::construction::{ DelaunayTriangulation, DelaunayTriangulationConstructionError, ExplicitConstructionError, TopologyGuarantee, vertex, }; +use delaunay::prelude::geometry::AdaptiveKernel; +use delaunay::prelude::query::BoundaryAnalysis; +use delaunay::prelude::tds::Tds; use delaunay::topology::characteristics::{euler, validation}; use delaunay::topology::traits::topological_space::{ GlobalTopology, TopologyKind, ToroidalConstructionMode, }; -use delaunay::triangulation::builder::DelaunayTriangulationBuilder; // ============================================================================= // DETERMINISTIC TESTS - KNOWN CONFIGURATIONS diff --git a/tests/example_workflows.rs b/tests/example_workflows.rs index 1861f0e1..d2bfc29a 100644 --- a/tests/example_workflows.rs +++ b/tests/example_workflows.rs @@ -2,10 +2,10 @@ //! Integration tests for workflows demonstrated by runnable examples. -use delaunay::prelude::query::{ConvexHull, Coordinate, Point}; -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ DelaunayTriangulation, DelaunayTriangulationConstructionError, vertex, }; +use delaunay::prelude::query::{ConvexHull, Coordinate, Point}; #[test] fn triangulation_and_hull_workflow_remains_valid() -> Result<(), WorkflowTestError> { @@ -54,9 +54,7 @@ enum WorkflowTestError { #[error(transparent)] Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] - Validation( - #[from] delaunay::prelude::triangulation::validation::DelaunayTriangulationValidationError, - ), + Validation(#[from] delaunay::prelude::validation::DelaunayTriangulationValidationError), #[error(transparent)] AdjacencyIndex(#[from] delaunay::prelude::query::AdjacencyIndexBuildError), #[error("convex hull construction failed: {source}")] diff --git a/tests/insert_with_statistics.rs b/tests/insert_with_statistics.rs index 958f48a6..e0ac0db5 100644 --- a/tests/insert_with_statistics.rs +++ b/tests/insert_with_statistics.rs @@ -14,10 +14,8 @@ //! - Bootstrap phase (< D+1 vertices) //! - Post-bootstrap phase (≥ D+1 vertices) -use delaunay::prelude::triangulation::construction::{ - DelaunayTriangulation, TopologyGuarantee, vertex, -}; -use delaunay::prelude::triangulation::insertion::{InsertionError, InsertionOutcome}; +use delaunay::prelude::construction::{DelaunayTriangulation, TopologyGuarantee, vertex}; +use delaunay::prelude::insertion::{InsertionError, InsertionOutcome}; // ============================================================================= // DELAUNAY TRIANGULATION TESTS diff --git a/tests/large_scale_debug.rs b/tests/large_scale_debug.rs index e64f856c..45717363 100644 --- a/tests/large_scale_debug.rs +++ b/tests/large_scale_debug.rs @@ -17,7 +17,7 @@ //! the same `[n] [repair_every]` shape. The ignored test defaults are: //! //! - 2D: 40,000 vertices -//! - 3D: 8,000 vertices +//! - 3D: 7,500 vertices //! - 4D: 900 vertices //! - 5D: 150 vertices //! @@ -95,20 +95,18 @@ use delaunay::geometry::kernel::{ExactPredicates, Kernel, RobustKernel}; use delaunay::geometry::util::{ generate_random_points_in_ball_seeded, generate_random_points_seeded, safe_usize_to_scalar, }; -use delaunay::prelude::tds::{InvariantKind, TriangulationValidationReport}; -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ ConstructionOptions, ConstructionStatistics, DelaunayRepairPolicy, DelaunayTriangulation, DelaunayTriangulationConstructionErrorWithStatistics, InitialSimplexStrategy, TopologyGuarantee, Vertex, vertex, }; -use delaunay::prelude::triangulation::diagnostics::ConstructionTelemetry; +use delaunay::prelude::diagnostics::ConstructionTelemetry; #[cfg(feature = "diagnostics")] -use delaunay::prelude::triangulation::insertion::InsertionResult; -use delaunay::prelude::triangulation::insertion::{InsertionOutcome, InsertionStatistics}; -use delaunay::prelude::triangulation::repair::{ - DelaunayCheckPolicy, DelaunayRepairHeuristicConfig, -}; -use delaunay::prelude::triangulation::validation::ValidationCadence; +use delaunay::prelude::insertion::InsertionResult; +use delaunay::prelude::insertion::{InsertionOutcome, InsertionStatistics}; +use delaunay::prelude::repair::{DelaunayCheckPolicy, DelaunayRepairHeuristicConfig}; +use delaunay::prelude::tds::{InvariantKind, TriangulationValidationReport}; +use delaunay::prelude::validation::ValidationCadence; use rand::{SeedableRng, rngs::StdRng, seq::SliceRandom}; use std::env; use std::fmt; @@ -1759,7 +1757,7 @@ fn debug_large_scale_2d() { #[test] #[ignore = "large-scale debug harness (manual run)"] fn debug_large_scale_3d() { - let outcome = debug_large_case::<3>("3D", 8_000); + let outcome = debug_large_case::<3>("3D", 7_500); assert!(matches!(outcome, DebugOutcome::Success), "{outcome}"); } diff --git a/tests/pachner_roundtrip.rs b/tests/pachner_roundtrip.rs index 64bbc73f..0c356595 100644 --- a/tests/pachner_roundtrip.rs +++ b/tests/pachner_roundtrip.rs @@ -2,13 +2,13 @@ //! Public API roundtrip tests for Pachner/bistellar flips. -use delaunay::prelude::geometry::RobustKernel; -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ ConstructionOptions, DelaunayTriangulation, InsertionOrderStrategy, TopologyGuarantee, vertex, }; -use delaunay::prelude::triangulation::flips::{ +use delaunay::prelude::flips::{ BistellarFlips, EdgeKey, FacetHandle, RidgeHandle, SimplexKey, TriangleHandle, VertexKey, }; +use delaunay::prelude::geometry::RobustKernel; use uuid::Uuid; type Dt4 = DelaunayTriangulation, (), (), 4>; diff --git a/tests/prelude_exports.rs b/tests/prelude_exports.rs index b5f9ad0e..c4d9b6f0 100644 --- a/tests/prelude_exports.rs +++ b/tests/prelude_exports.rs @@ -17,54 +17,73 @@ use delaunay::prelude::collections::SimplexKeyBuffer; use delaunay::prelude::collections::{ SecureHashMap as ScopedSecureHashMap, SecureHashSet as ScopedSecureHashSet, }; +use delaunay::prelude::construction::{ + CavityFillingError, CavityRepairStage, ConstructionOptions, ConstructionSkipSample, + ConstructionSlowInsertionSample, DelaunayConstructionFailure, DelaunayRepairPolicy, + DelaunayTriangulation, DelaunayTriangulationConstructionError, ExplicitConstructionError, + ExplicitDelaunayValidationError, ExplicitDelaunayValidationErrorKind, + ExplicitDelaunayValidationSourceKind, ExplicitInsertionError, ExplicitInsertionErrorKind, + ExplicitInvariantError, ExplicitInvariantErrorKind, ExplicitTdsError, ExplicitTdsErrorKind, + InsertionOrderStrategy, SimplexValidationError, TopologyGuarantee, Vertex, vertex, +}; +use delaunay::prelude::delaunayize::{ + DelaunayizeConfig, DelaunayizeError, DelaunayizeOutcome, delaunayize_by_flips, +}; +use delaunay::prelude::diagnostics::ConstructionTelemetry; #[cfg(feature = "diagnostics")] use delaunay::prelude::diagnostics::{ DelaunayViolationDetail, DelaunayViolationReport, NeighborSlot as DiagnosticNeighborSlot, debug_print_first_delaunay_violation, delaunay_violation_report, verify_conflict_region_completeness, }; +use delaunay::prelude::flips::BistellarFlips; use delaunay::prelude::generators::{RandomPointGenerationError, generate_random_points_seeded}; #[cfg(feature = "diagnostics")] -use delaunay::prelude::geometry::Coordinate; +use delaunay::prelude::geometry::{AdaptiveKernel, Coordinate}; use delaunay::prelude::geometry::{ - AdaptiveKernel, CoordinateConversionError, DegenerateSimplexReason, MatrixError, Point, + CoordinateConversionError, DegenerateSimplexReason, MatrixError, Point, +}; +use delaunay::prelude::insertion::{ + InsertionError, NeighborRebuildError, Tds as InsertionTds, TdsMutationError, + repair_neighbor_pointers_local, }; use delaunay::prelude::ordering::{ HilbertError, hilbert_index, hilbert_indices_prequantized, hilbert_quantize, hilbert_sort_by_stable, hilbert_sort_by_unstable, hilbert_sorted_indices, }; use delaunay::prelude::query::ConvexHull; -#[cfg(feature = "diagnostics")] -use delaunay::prelude::tds::Tds; -use delaunay::prelude::tds::{InvariantErrorSummaryDetail, NeighborSlot, TdsErrorKind}; -use delaunay::prelude::triangulation::construction::{ - CavityFillingError, CavityRepairStage, ConstructionOptions, ConstructionSkipSample, - ConstructionSlowInsertionSample, DelaunayConstructionFailure, DelaunayRepairPolicy, - DelaunayTriangulation, DelaunayTriangulationConstructionError, ExplicitConstructionError, - ExplicitDelaunayValidationError, ExplicitDelaunayValidationErrorKind, - ExplicitDelaunayValidationSourceKind, ExplicitInsertionError, ExplicitInsertionErrorKind, - ExplicitInvariantError, ExplicitInvariantErrorKind, ExplicitTdsError, ExplicitTdsErrorKind, - InsertionOrderStrategy, SimplexValidationError, TopologyGuarantee, Vertex, vertex, -}; -use delaunay::prelude::triangulation::delaunayize::{ - DelaunayizeConfig, DelaunayizeError, DelaunayizeOutcome, delaunayize_by_flips, -}; -use delaunay::prelude::triangulation::diagnostics::ConstructionTelemetry; -use delaunay::prelude::triangulation::flips::BistellarFlips; -use delaunay::prelude::triangulation::insertion::{ - InsertionError, NeighborRebuildError, Tds as InsertionTds, TdsMutationError, - repair_neighbor_pointers_local, -}; -use delaunay::prelude::triangulation::repair::{ +use delaunay::prelude::repair::{ DelaunayCheckPolicy, DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairOperation, DelaunayRepairOutcome, DelaunayRepairStats, DelaunayRepairVerificationContext, DelaunayTriangulationValidationError, FlipEdgeAdjacencyError, FlipError, FlipTriangleAdjacencyError, FlipVertexAdjacencyError, RepairQueueOrder, verify_delaunay_for_triangulation, }; -use delaunay::prelude::triangulation::validation::ValidationCadence; +#[cfg(feature = "diagnostics")] +use delaunay::prelude::tds::Tds; +use delaunay::prelude::tds::{InvariantErrorSummaryDetail, NeighborSlot, TdsErrorKind}; +use delaunay::prelude::triangulation::{ + FacetIssuesMap as TriangulationFacetIssuesMap, FastKernel as TriangulationFastKernel, + InsertionError as TriangulationInsertionError, TdsError as TriangulationTdsError, + TopologyGuarantee as TriangulationTopologyGuarantee, Triangulation as GenericTriangulation, + TriangulationConstructionError as GenericTriangulationConstructionError, + ValidationPolicy as TriangulationValidationPolicy, vertex as triangulation_vertex, +}; +use delaunay::prelude::validation::ValidationCadence; use delaunay::prelude::{SecureHashMap, SecureHashSet}; +#[derive(Debug, thiserror::Error)] +enum RootApiExportTestError { + #[error(transparent)] + Construction(#[from] delaunay::DelaunayTriangulationConstructionError), + #[error(transparent)] + Validation(#[from] delaunay::DelaunayTriangulationValidationError), + #[error(transparent)] + DelaunayRepair(#[from] delaunay::flips::DelaunayRepairError), + #[error(transparent)] + Delaunayize(#[from] delaunay::delaunayize::DelaunayizeError), +} + #[derive(Debug, thiserror::Error)] enum PreludeExportTestError { #[error(transparent)] @@ -82,10 +101,68 @@ enum PreludeExportTestError { } /// Proves the focused flips prelude exports the trait bound expected by benchmarks. -const fn assert_bistellar_flips(_: &impl BistellarFlips, (), 3>) {} +const fn assert_bistellar_flips(_: &impl BistellarFlips<3, Scalar = f64, VertexData = ()>) {} + +/// Proves the root flips module exports the same public trait bound. +const fn assert_root_bistellar_flips( + _: &impl delaunay::flips::BistellarFlips<3, Scalar = f64, VertexData = ()>, +) { +} const fn assert_send_sync_unpin() {} +#[test] +fn root_exports_cover_flattened_public_api() -> Result<(), RootApiExportTestError> { + use delaunay::builder::DelaunayTriangulationBuilder as BuilderModuleBuilder; + use delaunay::construction::{ + ConstructionOptions as ConstructionModuleOptions, InsertionOrderStrategy, + }; + use delaunay::delaunayize::{ + DelaunayizeConfig as DelaunayizeModuleConfig, delaunayize_by_flips, + }; + use delaunay::repair::{DelaunayCheckPolicy, DelaunayRepairPolicy}; + use delaunay::validation::{DelaunayTriangulationValidationError, ValidationCadence}; + use delaunay::{ + ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationBuilder, + TopologyGuarantee, ValidationPolicy, + }; + + 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 options: ConstructionOptions = + ConstructionModuleOptions::default().with_insertion_order(InsertionOrderStrategy::Input); + let builder: BuilderModuleBuilder<'_, f64, (), 3> = + DelaunayTriangulationBuilder::new(&vertices).construction_options(options); + let mut dt: DelaunayTriangulation<_, (), (), 3> = builder.build::<()>()?; + + assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); + assert_eq!(dt.validation_policy(), ValidationPolicy::OnSuspicion); + assert!(matches!( + ValidationCadence::from_optional_every(Some(2)), + ValidationCadence::EveryN(every) if every.get() == 2 + )); + assert_eq!( + DelaunayRepairPolicy::default(), + DelaunayRepairPolicy::EveryInsertion + ); + assert!(!DelaunayCheckPolicy::default().should_check(1)); + + let validation_result: Result<(), DelaunayTriangulationValidationError> = dt.validate(); + validation_result?; + assert_bistellar_flips(&dt); + assert_root_bistellar_flips(&dt); + + let outcome = delaunayize_by_flips(&mut dt, DelaunayizeModuleConfig::default())?; + assert!(!outcome.used_fallback_rebuild); + assert!(outcome.topology_repair.succeeded); + Ok(()) +} + #[test] fn preludes_cover_bench_apis() -> Result<(), PreludeExportTestError> { let _generated_points: Vec> = generate_random_points_seeded(3, (0.0, 1.0), 42)?; @@ -237,6 +314,38 @@ fn construction_prelude_covers_explicit_error_summaries() { ); } +#[test] +fn triangulation_prelude_covers_generic_layer() -> Result<(), GenericTriangulationConstructionError> +{ + let vertices = vec![ + triangulation_vertex!([0.0, 0.0]), + triangulation_vertex!([1.0, 0.0]), + triangulation_vertex!([0.0, 1.0]), + ]; + let tds = + GenericTriangulation::, (), (), 2>::build_initial_simplex( + &vertices, + )?; + assert_eq!(tds.number_of_vertices(), 3); + assert_eq!(tds.number_of_simplices(), 1); + + let mut tri: GenericTriangulation, (), (), 2> = + GenericTriangulation::new_empty(TriangulationFastKernel::new()); + tri.set_topology_guarantee(TriangulationTopologyGuarantee::Pseudomanifold); + tri.set_validation_policy(TriangulationValidationPolicy::Never); + assert!(tri.validate().is_ok()); + + let empty_issues = TriangulationFacetIssuesMap::default(); + let removed = tri + .repair_local_facet_issues(&empty_issues) + .expect("empty issue set should not fail generic local repair"); + assert_eq!(removed, 0); + + assert_send_sync_unpin::(); + assert_send_sync_unpin::(); + Ok(()) +} + #[test] fn diagnostic_preludes_cover_repair_apis() -> Result<(), PreludeExportTestError> { let vertices: Vec> = vec![ diff --git a/tests/proptest_delaunay_triangulation.rs b/tests/proptest_delaunay_triangulation.rs index 1ff9c12f..dff80270 100644 --- a/tests/proptest_delaunay_triangulation.rs +++ b/tests/proptest_delaunay_triangulation.rs @@ -32,14 +32,14 @@ //! This provides ~40-100x speedup for property-based testing while remaining equally correct. use delaunay::geometry::kernel::{AdaptiveKernel, RobustKernel}; -use delaunay::prelude::geometry::*; -use delaunay::prelude::tds::TdsError; -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ ConstructionOptions, DedupPolicy, DelaunayRepairPolicy, DelaunayTriangulation, TopologyGuarantee, Vertex, vertex, }; -use delaunay::prelude::triangulation::insertion::{InsertionError, InsertionOutcome}; -use delaunay::prelude::triangulation::validation::ValidationPolicy; +use delaunay::prelude::geometry::*; +use delaunay::prelude::insertion::{InsertionError, InsertionOutcome}; +use delaunay::prelude::tds::TdsError; +use delaunay::prelude::validation::ValidationPolicy; use proptest::prelude::*; use proptest::test_runner::{Config, TestCaseError, TestRunner}; use rand::{SeedableRng, seq::SliceRandom}; diff --git a/tests/proptest_euler_characteristic.rs b/tests/proptest_euler_characteristic.rs index e3108b87..feff2d55 100644 --- a/tests/proptest_euler_characteristic.rs +++ b/tests/proptest_euler_characteristic.rs @@ -17,9 +17,7 @@ //! For deterministic tests with known configurations, see `euler_characteristic.rs`. use delaunay::geometry::util::generate_random_triangulation_with_topology_guarantee; -use delaunay::prelude::triangulation::construction::{ - DelaunayTriangulation, TopologyGuarantee, vertex, -}; +use delaunay::prelude::construction::{DelaunayTriangulation, TopologyGuarantee, vertex}; use delaunay::topology::characteristics::{euler, validation}; use proptest::prelude::*; diff --git a/tests/proptest_flips.rs b/tests/proptest_flips.rs index de85bc10..aefb43a9 100644 --- a/tests/proptest_flips.rs +++ b/tests/proptest_flips.rs @@ -9,13 +9,13 @@ //! dimensions 2D-5D. use ::uuid::Uuid; +use delaunay::prelude::construction::{ + DelaunayTriangulation, TopologyGuarantee, Triangulation, Vertex, +}; +use delaunay::prelude::flips::BistellarFlips; use delaunay::prelude::geometry::{ AdaptiveKernel, Coordinate, FastKernel, Kernel, Point, RobustKernel, }; -use delaunay::prelude::triangulation::construction::{ - DelaunayTriangulation, TopologyGuarantee, Triangulation, Vertex, -}; -use delaunay::prelude::triangulation::flips::BistellarFlips; use proptest::prelude::*; use std::collections::{BTreeSet, HashMap}; diff --git a/tests/proptest_orientation.rs b/tests/proptest_orientation.rs index a3d56874..cfe92361 100644 --- a/tests/proptest_orientation.rs +++ b/tests/proptest_orientation.rs @@ -10,12 +10,10 @@ #![forbid(unsafe_code)] +use delaunay::prelude::construction::{DelaunayTriangulation, TopologyGuarantee, Vertex}; use delaunay::prelude::geometry::*; +use delaunay::prelude::insertion::InsertionOutcome; use delaunay::prelude::tds::{Tds, TdsError}; -use delaunay::prelude::triangulation::construction::{ - DelaunayTriangulation, TopologyGuarantee, Vertex, -}; -use delaunay::prelude::triangulation::insertion::InsertionOutcome; use proptest::prelude::*; /// Strategy for generating finite `f64` coordinates in a reasonable range. diff --git a/tests/proptest_triangulation.rs b/tests/proptest_triangulation.rs index d47f167a..9f82249d 100644 --- a/tests/proptest_triangulation.rs +++ b/tests/proptest_triangulation.rs @@ -35,11 +35,9 @@ //! Tests are generated for dimensions 2D-5D using macros to reduce duplication. use ::uuid::Uuid; +use delaunay::prelude::construction::{DelaunayTriangulation, TopologyGuarantee, Vertex, vertex}; use delaunay::prelude::geometry::*; use delaunay::prelude::tds::SimplexKey; -use delaunay::prelude::triangulation::construction::{ - DelaunayTriangulation, TopologyGuarantee, Vertex, vertex, -}; use proptest::prelude::*; use proptest::test_runner::{Config, TestCaseError, TestRunner}; use std::cell::RefCell; diff --git a/tests/proptest_vertex.rs b/tests/proptest_vertex.rs index 782392b0..6782a530 100644 --- a/tests/proptest_vertex.rs +++ b/tests/proptest_vertex.rs @@ -12,8 +12,8 @@ #![allow(unused_imports)] // Imports used in macro expansion +use delaunay::prelude::construction::{Vertex, vertex}; use delaunay::prelude::geometry::Point; -use delaunay::prelude::triangulation::construction::{Vertex, vertex}; use proptest::prelude::*; use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; diff --git a/tests/public_topology_api.rs b/tests/public_topology_api.rs index a85441fc..a40bc960 100644 --- a/tests/public_topology_api.rs +++ b/tests/public_topology_api.rs @@ -8,9 +8,7 @@ use delaunay::prelude::TopologyGuarantee; use delaunay::prelude::query::*; -use delaunay::prelude::triangulation::{ - DelaunayTriangulationConstructionError, construction::vertex, -}; +use delaunay::prelude::{DelaunayTriangulationConstructionError, construction::vertex}; use std::collections::HashSet; #[derive(Debug, thiserror::Error)] diff --git a/tests/regressions.rs b/tests/regressions.rs index c8cc46dc..2893098b 100644 --- a/tests/regressions.rs +++ b/tests/regressions.rs @@ -4,18 +4,18 @@ //! integration test crates, unless the case needs separate crate-level setup, //! feature flags, or profile isolation. +use delaunay::prelude::construction::{ + ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationBuilder, + InsertionOrderStrategy, RetryPolicy, TopologyGuarantee, Vertex, 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::{Point, RobustKernel}; -use delaunay::prelude::ordering::{hilbert_indices_prequantized, hilbert_quantize}; -use delaunay::prelude::triangulation::construction::{ - ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationBuilder, - InsertionOrderStrategy, RetryPolicy, TopologyGuarantee, Vertex, vertex, -}; -use delaunay::prelude::triangulation::insertion::{ +use delaunay::prelude::insertion::{ HullExtensionReason, InsertionError, InsertionErrorKind, InsertionErrorSummary, }; +use delaunay::prelude::ordering::{hilbert_indices_prequantized, hilbert_quantize}; /// Replays a full Hilbert ordering while keeping only the prefix that first /// exposed issue #307, so the regression stays fast and deterministic. diff --git a/tests/semgrep/src/project_rules/rust_style.rs b/tests/semgrep/src/project_rules/rust_style.rs index 7ba101b3..7c7e2a58 100644 --- a/tests/semgrep/src/project_rules/rust_style.rs +++ b/tests/semgrep/src/project_rules/rust_style.rs @@ -5,7 +5,7 @@ use num_traits::NumCast; // 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 -use delaunay::prelude::triangulation::Vertex as PreludeVertex; +use delaunay::prelude::Vertex as PreludeVertex; pub fn production_stdio() { // ruleid: delaunay.rust.no-stdio-diagnostics-in-src @@ -131,13 +131,13 @@ enum PrivateFixtureError { fn doctest_style_error_is_ignored() {} /// ```rust -/// // ruleid: delaunay.rust.prefer-prelude-imports-in-triangulation-doctests -/// use delaunay::triangulation::delaunay::DelaunayTriangulation; -/// // ok: delaunay.rust.prefer-prelude-imports-in-triangulation-doctests -/// use delaunay::prelude::triangulation::DelaunayTriangulation; -/// // ok: delaunay.rust.prefer-prelude-imports-in-triangulation-doctests -/// # use delaunay::prelude::triangulation::DelaunayTriangulation as HiddenPreludeImport; -/// // ruleid: delaunay.rust.prefer-prelude-imports-in-triangulation-doctests -/// # use delaunay::triangulation::delaunay::DelaunayTriangulation as HiddenDeepImport; +/// // ruleid: delaunay.rust.prefer-prelude-imports-in-delaunay-doctests +/// use delaunay::flips::BistellarFlips; +/// // ok: delaunay.rust.prefer-prelude-imports-in-delaunay-doctests +/// use delaunay::prelude::DelaunayTriangulation; +/// // ok: delaunay.rust.prefer-prelude-imports-in-delaunay-doctests +/// # use delaunay::prelude::DelaunayTriangulation as HiddenPreludeImport; +/// // ruleid: delaunay.rust.prefer-prelude-imports-in-delaunay-doctests +/// # use delaunay::flips::BistellarFlips as HiddenDeepImport; /// ``` fn triangulation_doctest_deep_import_fixture() {} diff --git a/tests/serialization_vertex_preservation.rs b/tests/serialization_vertex_preservation.rs index b830cee7..95c85185 100644 --- a/tests/serialization_vertex_preservation.rs +++ b/tests/serialization_vertex_preservation.rs @@ -10,12 +10,12 @@ //! or a bug in serialization/deserialization. use delaunay::assert_jaccard_gte; +use delaunay::prelude::construction::{ + ConstructionOptions, DelaunayTriangulation, InsertionOrderStrategy, TopologyGuarantee, Vertex, +}; use delaunay::prelude::geometry::*; use delaunay::prelude::query::extract_vertex_coordinate_set; use delaunay::prelude::tds::Tds; -use delaunay::prelude::triangulation::construction::{ - ConstructionOptions, DelaunayTriangulation, InsertionOrderStrategy, TopologyGuarantee, Vertex, -}; use std::collections::HashSet; /// Test vertex preservation with duplicate coordinates diff --git a/tests/trait_bound_ergonomics.rs b/tests/trait_bound_ergonomics.rs index 6cbc238f..4c06217e 100644 --- a/tests/trait_bound_ergonomics.rs +++ b/tests/trait_bound_ergonomics.rs @@ -1,10 +1,10 @@ //! Compile coverage for read-only APIs with non-`DataType` payloads. +use delaunay::prelude::Triangulation; use delaunay::prelude::geometry::FastKernel; use delaunay::prelude::query::BoundaryAnalysis; use delaunay::prelude::tds::{Simplex, SimplexKey, Tds, verify_facet_index_consistency}; use delaunay::prelude::topology::validation::validate_triangulation_euler; -use delaunay::prelude::triangulation::Triangulation; struct Payload; diff --git a/tests/triangulation_builder.rs b/tests/triangulation_builder.rs index 13d022eb..4528fabf 100644 --- a/tests/triangulation_builder.rs +++ b/tests/triangulation_builder.rs @@ -1,24 +1,24 @@ //! Integration tests for [`DelaunayTriangulationBuilder`]. //! //! These tests exercise the public API from the outside, using only items exposed -//! through `delaunay::prelude::triangulation` and `delaunay::triangulation::builder`. +//! through `delaunay::prelude` and `delaunay::builder`. #![forbid(unsafe_code)] use std::collections::HashMap; use std::f64::consts::TAU; -use delaunay::prelude::geometry::{Coordinate, Point, RobustKernel}; -use delaunay::prelude::tds::{InvariantErrorSummaryDetail, TriangulationValidationErrorKind}; -use delaunay::prelude::topology::spaces::{GlobalTopology, TopologyKind, ToroidalConstructionMode}; -use delaunay::prelude::topology::validation::{count_simplices, euler_characteristic}; -use delaunay::prelude::triangulation::construction::{ +use delaunay::prelude::construction::{ ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, ExplicitConstructionError, ExplicitInsertionError, ExplicitInsertionErrorKind, ExplicitInvariantError, ExplicitInvariantErrorKind, ExplicitTdsErrorKind, InsertionOrderStrategy, TopologyGuarantee, Vertex, VertexBuilder, vertex, }; -use delaunay::prelude::triangulation::repair::DelaunayRepairError; +use delaunay::prelude::geometry::{Coordinate, Point, RobustKernel}; +use delaunay::prelude::repair::DelaunayRepairError; +use delaunay::prelude::tds::{InvariantErrorSummaryDetail, TriangulationValidationErrorKind}; +use delaunay::prelude::topology::spaces::{GlobalTopology, TopologyKind, ToroidalConstructionMode}; +use delaunay::prelude::topology::validation::{count_simplices, euler_characteristic}; // ============================================================================= // Euclidean path From b673f2faf07fc6ee4ab2237efba6252ec33cd8ff Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Tue, 19 May 2026 09:07:41 -0700 Subject: [PATCH 2/3] fix!: return typed errors for boundary and repair failures - Return `QueryError` from boundary facet queries instead of panicking when facet-map construction detects corrupted topology. - Add a simplex-removal budget to local facet repair and preserve budget-exceeded failures through insertion, construction, and flip-wiring error paths. - Report orientation-promotion non-convergence as a typed validation failure with residual simplex diagnostics. - Update examples, preludes, benchmarks, and workflow/tooling pins for the fallible query and repair APIs. BREAKING CHANGE: `Triangulation::boundary_facets` and `DelaunayTriangulation::boundary_facets` now return `Result`. BREAKING CHANGE: `Triangulation::repair_local_facet_issues` now requires a `max_simplices_removed` argument and can return `InsertionError::MaxSimplicesRemovedExceeded`. --- .github/workflows/benchmarks.yml | 2 +- .github/workflows/ci.yml | 4 +- .github/workflows/generate-baseline.yml | 2 +- .github/workflows/semgrep-sarif.yml | 2 +- REFERENCES.md | 15 ++ benches/ci_performance_suite.rs | 9 +- docs/dev/tooling-alignment.md | 2 +- examples/triangulation_and_hull.rs | 5 +- semgrep.yaml | 8 +- src/core/adjacency.rs | 2 +- src/core/algorithms/flips.rs | 30 ++++ src/core/algorithms/incremental_insertion.rs | 34 +++- src/core/boundary.rs | 82 +++++---- src/core/construction.rs | 14 +- src/core/orientation.rs | 15 +- src/core/query.rs | 96 ++++++++-- src/core/repair.rs | 46 ++++- src/core/tds.rs | 7 +- src/core/traits/boundary_analysis.rs | 2 +- src/core/validation.rs | 15 ++ src/delaunay/builder.rs | 5 + src/delaunay/construction.rs | 90 ++++++++++ src/delaunay/insertion.rs | 17 +- src/delaunay/query.rs | 174 +++++++++++++++---- src/delaunay/repair.rs | 27 +-- src/delaunay/validation.rs | 3 + src/lib.rs | 6 +- tests/example_workflows.rs | 6 +- tests/prelude_exports.rs | 15 +- tests/proptest_triangulation.rs | 2 +- tests/trait_bound_ergonomics.rs | 2 +- tests/triangulation_builder.rs | 21 ++- 32 files changed, 618 insertions(+), 142 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 00e8ca48..363c758b 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -42,7 +42,7 @@ concurrency: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 - UV_VERSION: "0.11.14" + UV_VERSION: "0.11.15" BENCHMARK_TIMEOUT: 1800 # 30 min; pre-computed seeds + reduced 5D counts keep runtime well under this DELAUNAY_BENCH_DISCOVER_SEEDS_LIMIT: 256 # fallback only; ci_performance_suite uses pre-computed seeds diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b720028e..c0327699 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,14 +24,14 @@ env: ACTIONLINT_VERSION: "1.7.12" DPRINT_VERSION: "0.54.0" JUST_VERSION: "1.51.0" - RUMDL_VERSION: "0.1.93" + RUMDL_VERSION: "0.1.94" SHFMT_VERSION: "3.13.1" SHFMT_SHA256_DARWIN_AMD64: "6feedafc72915794163114f512348e2437d080d0047ef8b8fa2ec63b575f12af" SHFMT_SHA256_DARWIN_ARM64: "9680526be4a66ea1ffe988ed08af58e1400fe1e4f4aef5bd88b20bb9b3da33f8" SHFMT_SHA256_LINUX_AMD64: "fb096c5d1ac6beabbdbaa2874d025badb03ee07929f0c9ff67563ce8c75398b1" TAPLO_VERSION: "0.10.0" TYPOS_VERSION: "1.46.1" - UV_VERSION: "0.11.14" + UV_VERSION: "0.11.15" jobs: build: diff --git a/.github/workflows/generate-baseline.yml b/.github/workflows/generate-baseline.yml index a23ccdb4..1d519a24 100644 --- a/.github/workflows/generate-baseline.yml +++ b/.github/workflows/generate-baseline.yml @@ -34,7 +34,7 @@ permissions: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 - UV_VERSION: "0.11.14" + UV_VERSION: "0.11.15" # Seed search limit for both old (pre-v0.8) and current env var names. # Old tags read DELAUNAY_BENCH_SEED_SEARCH_LIMIT; current code reads # DELAUNAY_BENCH_DISCOVER_SEEDS_LIMIT. Setting both ensures backward diff --git a/.github/workflows/semgrep-sarif.yml b/.github/workflows/semgrep-sarif.yml index 28ab8d65..1d1f6d99 100644 --- a/.github/workflows/semgrep-sarif.yml +++ b/.github/workflows/semgrep-sarif.yml @@ -24,7 +24,7 @@ permissions: actions: read env: - UV_VERSION: "0.11.14" + UV_VERSION: "0.11.15" jobs: semgrep-sarif: diff --git a/REFERENCES.md b/REFERENCES.md index b068289c..2c3bea01 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -29,6 +29,21 @@ Zenodo. DOI: https://doi.org/10.5281/zenodo.16931097 For BibTeX, APA, or other citation formats, please refer to the [CITATION.cff](CITATION.cff) file or use GitHub's "Cite this repository" feature. +## Inline Citation Keys + +Some Rust API docs use compact numbered citations. The labels below point to +the corresponding bibliography entries in this file: + +- [1] Shewchuk, J. R. (1997), adaptive precision floating-point arithmetic and + robust geometric predicates. +- [2] Bowyer, A. (1981), incremental Dirichlet/Delaunay tessellation. +- [3] Watson, D. F. (1981), n-dimensional Delaunay tessellation by cavity + replacement. +- [4] Edelsbrunner, H., and Shah, N. R. (1996), topological flipping for + regular triangulations. +- [5] Edelsbrunner, H. (2001), mesh generation, cavity-based construction, and + triangulation repair background. + ## Advanced Computational Geometry Topics These references support specialized features and high-dimensional computations in the library. diff --git a/benches/ci_performance_suite.rs b/benches/ci_performance_suite.rs index b8750a1a..9bd1a51f 100644 --- a/benches/ci_performance_suite.rs +++ b/benches/ci_performance_suite.rs @@ -1032,7 +1032,14 @@ fn bench_boundary_case( count, ), |b| { - b.iter(|| black_box(dt.boundary_facets().count())); + b.iter(|| { + black_box(match dt.boundary_facets() { + Ok(facets) => facets.count(), + Err(error) => unreachable!( + "validated benchmark triangulation should build boundary facets: {error}" + ), + }) + }); }, ); } diff --git a/docs/dev/tooling-alignment.md b/docs/dev/tooling-alignment.md index 0c02874e..5c4ecc9b 100644 --- a/docs/dev/tooling-alignment.md +++ b/docs/dev/tooling-alignment.md @@ -93,7 +93,7 @@ The useful updates ported in this pass are: - CI and local setup pins should track the same supported tool versions when practical. The current workflow pins align coverage and test tooling on `cargo-llvm-cov` 0.8.7 and `cargo-nextest` 0.9.136, while all uv-backed - workflows use uv 0.11.14 to match the local Python tooling bootstrap. + workflows use uv 0.11.15 to match the local Python tooling bootstrap. ## Intentional Differences diff --git a/examples/triangulation_and_hull.rs b/examples/triangulation_and_hull.rs index e649666d..8a7f066d 100644 --- a/examples/triangulation_and_hull.rs +++ b/examples/triangulation_and_hull.rs @@ -22,6 +22,7 @@ use delaunay::prelude::generators::{RandomPointGenerationError, generate_random_ use delaunay::prelude::geometry::AdaptiveKernel; use delaunay::prelude::query::{ AdjacencyIndexBuildError, ConvexHull, ConvexHullConstructionError, Coordinate, Point, + QueryError, }; type WorkflowTriangulation = DelaunayTriangulation, (), (), D>; @@ -34,6 +35,8 @@ enum WorkflowExampleError { Construction(#[from] DelaunayTriangulationConstructionError), #[error(transparent)] AdjacencyIndex(#[from] AdjacencyIndexBuildError), + #[error(transparent)] + Query(#[from] QueryError), #[error("convex hull operation failed: {source}")] ConvexHull { #[source] @@ -86,7 +89,7 @@ fn run_case( let index = dt.build_adjacency_index()?; println!(" edges: {}", index.number_of_edges()); - println!(" boundary facets: {}", dt.boundary_facets().count()); + println!(" boundary facets: {}", dt.boundary_facets()?.count()); let hull = ConvexHull::from_triangulation(dt.as_triangulation())?; println!(" hull facets: {}", hull.number_of_facets()); diff --git a/semgrep.yaml b/semgrep.yaml index f0d1fa06..a2199935 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -554,7 +554,9 @@ rules: - "/benches/**/*.rs" - "/src/project_rules/**/*.rs" patterns: - - pattern-regex: '^\s*use\s+delaunay::(core|builder|construction|delaunayize|diagnostics|flips|geometry|repair|topology|validation)::' + - pattern-either: + - pattern-regex: '^\s*use\s+delaunay::(builder|construction|core|delaunayize|diagnostics|flips)::' + - pattern-regex: '^\s*use\s+delaunay::(geometry|insertion|repair|topology|validation)::' - pattern-not-regex: '^\s*use\s+delaunay::prelude::' - id: delaunay.rust.prefer-prelude-imports-in-delaunay-doctests @@ -575,7 +577,9 @@ rules: - "/src/**/*.rs" - "/tests/semgrep/src/project_rules/**/*.rs" patterns: - - pattern-regex: '^\s*//[/!]\s*(?:#\s*)?use\s+delaunay::(core|builder|construction|delaunayize|diagnostics|flips|geometry|repair|topology|validation)::' + - pattern-either: + - pattern-regex: '^\s*//[/!]\s*(?:#\s*)?use\s+delaunay::(builder|construction|core|delaunayize|diagnostics|flips)::' + - pattern-regex: '^\s*//[/!]\s*(?:#\s*)?use\s+delaunay::(geometry|insertion|repair|topology|validation)::' - pattern-not-regex: '^\s*//[/!]\s*(?:#\s*)?use\s+delaunay::prelude::' - id: delaunay.docs.check-before-fix-command-order diff --git a/src/core/adjacency.rs b/src/core/adjacency.rs index 1d015d59..50599c97 100644 --- a/src/core/adjacency.rs +++ b/src/core/adjacency.rs @@ -351,7 +351,7 @@ impl AdjacencyIndex { #[cfg(test)] mod tests { use super::*; - use crate::triangulation::DelaunayTriangulation; + use crate::DelaunayTriangulation; use crate::vertex; use slotmap::SlotMap; use std::collections::HashSet; diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index d08c9e22..88d91686 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -3284,6 +3284,16 @@ pub enum FlipNeighborWiringError { #[source] source: TriangulationValidationError, }, + /// Local repair would exceed its simplex-removal budget. + #[error( + "local repair removal budget reached flip neighbor wiring: attempted {attempted}, max {max_simplices_removed}" + )] + MaxSimplicesRemovedExceeded { + /// Maximum simplices allowed for removal. + max_simplices_removed: usize, + /// Number of simplices selected for removal. + attempted: usize, + }, } impl From for FlipNeighborWiringError { @@ -3321,6 +3331,13 @@ impl From for FlipNeighborWiringError { InsertionError::TopologyValidationFailed { message, source } => { Self::TopologyValidationFailed { message, source } } + InsertionError::MaxSimplicesRemovedExceeded { + max_simplices_removed, + attempted, + } => Self::MaxSimplicesRemovedExceeded { + max_simplices_removed, + attempted, + }, } } } @@ -13502,6 +13519,19 @@ mod tests { } other => panic!("expected preserved Delaunay repair reason, got {other:?}"), } + + let budget_wiring = + FlipNeighborWiringError::from(InsertionError::MaxSimplicesRemovedExceeded { + max_simplices_removed: 2, + attempted: 3, + }); + assert_eq!( + budget_wiring, + FlipNeighborWiringError::MaxSimplicesRemovedExceeded { + max_simplices_removed: 2, + attempted: 3, + } + ); } #[test] diff --git a/src/core/algorithms/incremental_insertion.rs b/src/core/algorithms/incremental_insertion.rs index 829234d0..abd2311e 100644 --- a/src/core/algorithms/incremental_insertion.rs +++ b/src/core/algorithms/incremental_insertion.rs @@ -599,6 +599,15 @@ impl From for InitialSimplexConstructionError { message: source.to_string(), } } + TriangulationConstructionError::LocalRepairBudgetExceeded { + max_simplices_removed, + attempted, + } => Self::UnexpectedInsertionStage { + message: format!( + "local repair budget exceeded during initial simplex construction: \ + attempted {attempted}, max {max_simplices_removed}" + ), + }, TriangulationConstructionError::FinalTopologyValidation { source, .. } => { Self::UnexpectedInsertionStage { message: source.to_string(), @@ -727,6 +736,8 @@ pub enum InsertionErrorKind { TopologyValidation, /// Triangulation-layer topology validation failed. TopologyValidationFailed, + /// Local repair would exceed its simplex-removal budget. + MaxSimplicesRemovedExceeded, } /// Nested discriminant preserved by an [`InsertionErrorSummary`]. @@ -868,6 +879,9 @@ impl From for InsertionErrorSummary { InsertionError::TopologyValidationFailed { .. } => { InsertionErrorKind::TopologyValidationFailed } + InsertionError::MaxSimplicesRemovedExceeded { .. } => { + InsertionErrorKind::MaxSimplicesRemovedExceeded + } }; let source_kind = match &source { InsertionError::DelaunayValidationFailed { source } => { @@ -1412,6 +1426,22 @@ pub enum InsertionError { #[source] source: TriangulationValidationError, }, + + /// Local facet repair would remove more simplices than the caller allowed. + /// + /// This is emitted by + /// [`Triangulation::repair_local_facet_issues`](crate::Triangulation::repair_local_facet_issues) + /// before neighbor repair or validation runs, so callers can retry with a + /// larger budget without committing a partial topology edit. + #[error( + "Local facet repair removal budget exceeded: would remove {attempted} simplices, maximum is {max_simplices_removed}" + )] + MaxSimplicesRemovedExceeded { + /// Maximum simplices the caller allowed this repair to remove. + max_simplices_removed: usize, + /// Number of simplices selected for removal. + attempted: usize, + }, } impl From for InsertionError { @@ -1556,7 +1586,8 @@ impl InsertionError { | Self::DelaunayValidationFailed { .. } | Self::DelaunayRepairFailed { .. } | Self::DuplicateCoordinates { .. } - | Self::DuplicateUuid { .. } => false, + | Self::DuplicateUuid { .. } + | Self::MaxSimplicesRemovedExceeded { .. } => false, } } @@ -1643,6 +1674,7 @@ impl InsertionError { | TriangulationValidationError::BoundaryRidgeMultiplicity { .. } | TriangulationValidationError::RidgeLinkNotManifold { .. } | TriangulationValidationError::VertexLinkNotManifold { .. } + | TriangulationValidationError::OrientationPromotionNonConvergence { .. } | TriangulationValidationError::IsolatedVertex { .. } => true, // All other variants (structural invariant violations, future additions) // are conservatively treated as non-retryable. diff --git a/src/core/boundary.rs b/src/core/boundary.rs index 580e7357..fec6399f 100644 --- a/src/core/boundary.rs +++ b/src/core/boundary.rs @@ -81,8 +81,8 @@ impl BoundaryAnalysis for Tds { /// ]; /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); /// - /// // High-level API (infallible): panics if the underlying TDS is corrupted. - /// assert_eq!(dt.boundary_facets().count(), 4); + /// // High-level API returns `QueryError` if the underlying TDS is corrupted. + /// assert_eq!(dt.boundary_facets().unwrap().count(), 4); /// /// // TDS-level API (fallible): returns `TdsError` on corruption. /// let count = dt.tds().boundary_facets()?.count(); @@ -136,7 +136,7 @@ impl BoundaryAnalysis for Tds { /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); /// /// // Get boundary facets using the new iterator API - /// let first_facet = dt.boundary_facets().next().unwrap(); + /// let first_facet = dt.boundary_facets().unwrap().next().unwrap(); /// // In a single tetrahedron, all facets are boundary facets /// assert!(dt.tds().is_boundary_facet(&first_facet).unwrap()); /// ``` @@ -184,7 +184,7 @@ impl BoundaryAnalysis for Tds { /// .expect("Should build facet map"); /// /// // Check boundary facets efficiently using the iterator API - /// for facet in dt.boundary_facets() { + /// for facet in dt.boundary_facets().unwrap() { /// let is_boundary = dt.tds().is_boundary_facet_with_map(&facet, &facet_to_simplices) /// .expect("Should check if facet is boundary"); /// println!("Facet is boundary: {is_boundary}"); @@ -255,6 +255,7 @@ mod tests { use super::{BoundaryAnalysis, number_of_boundary_facets_in_map}; use crate::core::collections::FacetToSimplicesMap; use crate::core::facet::{FacetError, FacetHandle}; + use crate::core::query::QueryError; use crate::core::tds::{SimplexKey, TdsError}; use crate::core::vertex::Vertex; use crate::geometry::{point::Point, traits::coordinate::Coordinate}; @@ -296,7 +297,7 @@ mod tests { ); assert_eq!(dt.dim(), 2, "Should be 2-dimensional"); - let boundary_count = dt.boundary_facets().count(); + let boundary_count = dt.boundary_facets().unwrap().count(); assert_eq!( boundary_count, 3, "2D triangle should have 3 boundary facets" @@ -307,7 +308,7 @@ mod tests { .tds() .build_facet_to_simplices_map() .expect("Should build facet map"); - assert!(dt.boundary_facets().all(|f| { + assert!(dt.boundary_facets().unwrap().all(|f| { dt.tds() .is_boundary_facet_with_map(&f, &facet_to_simplices) .expect("Should not fail for valid facets") @@ -332,7 +333,7 @@ mod tests { ); assert_eq!(dt.dim(), 3, "Should be 3-dimensional"); - let boundary_count = dt.boundary_facets().count(); + let boundary_count = dt.boundary_facets().unwrap().count(); assert_eq!( boundary_count, 4, "3D tetrahedron should have 4 boundary facets" @@ -343,7 +344,7 @@ mod tests { .tds() .build_facet_to_simplices_map() .expect("Should build facet map"); - assert!(dt.boundary_facets().all(|f| { + assert!(dt.boundary_facets().unwrap().all(|f| { dt.tds() .is_boundary_facet_with_map(&f, &facet_to_simplices) .expect("Should not fail for valid facets") @@ -369,7 +370,7 @@ mod tests { ); assert_eq!(dt.dim(), 4, "Should be 4-dimensional"); - let boundary_count = dt.boundary_facets().count(); + let boundary_count = dt.boundary_facets().unwrap().count(); assert_eq!( boundary_count, 5, "4D simplex should have 5 boundary facets" @@ -382,6 +383,7 @@ mod tests { .expect("Should build facet map"); let confirmed_boundary = dt .boundary_facets() + .unwrap() .filter(|f| { dt.tds() .is_boundary_facet_with_map(f, &facet_to_simplices) @@ -403,7 +405,7 @@ mod tests { "Empty triangulation should have no simplices" ); - let boundary_count = dt.boundary_facets().count(); + let boundary_count = dt.boundary_facets().unwrap().count(); assert_eq!( boundary_count, 0, "Empty triangulation should have no boundary facets" @@ -431,14 +433,14 @@ mod tests { let dt = DelaunayTriangulation::new(&vertices).unwrap(); // Test boundary_facets() normal path - let boundary_count = dt.boundary_facets().count(); + let boundary_count = dt.boundary_facets().unwrap().count(); assert_eq!( boundary_count, 4, "Single tetrahedron has 4 boundary facets" ); // Test is_boundary_facet() delegation (builds facet map internally) - if let Some(facet) = dt.boundary_facets().next() { + if let Some(facet) = dt.boundary_facets().unwrap().next() { let result = dt.tds().is_boundary_facet(&facet); assert!(result.is_ok(), "Should not error on valid facet"); assert!( @@ -467,7 +469,7 @@ mod tests { ); // Exercise capacity allocation, cache initialization, and vector push operations - let boundary_count = dt.boundary_facets().count(); + let boundary_count = dt.boundary_facets().unwrap().count(); assert!(boundary_count > 0, "Should have boundary facets"); assert!( boundary_count >= 4, @@ -530,11 +532,11 @@ mod tests { // Time boundary_facets() method let start = Instant::now(); - let boundary_count_direct = dt.boundary_facets().count(); + let boundary_count_direct = dt.boundary_facets().unwrap().count(); let boundary_facets_time = start.elapsed(); // Collect facets for multiple operations - let boundary_facets_vec: Vec<_> = dt.boundary_facets().collect(); + let boundary_facets_vec: Vec<_> = dt.boundary_facets().unwrap().collect(); let boundary_len = boundary_facets_vec.len(); // Time is_boundary_facet() for each boundary facet @@ -624,7 +626,7 @@ mod tests { for _ in 0..runs { let start = Instant::now(); - let boundary_facets = dt.boundary_facets(); + let boundary_facets = dt.boundary_facets().unwrap(); total_time += start.elapsed(); // Prevent optimization away @@ -633,7 +635,7 @@ mod tests { let avg_time = total_time / runs; - let boundary_count = dt.boundary_facets().count(); + let boundary_count = dt.boundary_facets().unwrap().count(); println!( "Points: {:3} | Simplices: {:4} | Boundary Facets: {:4} | Avg Time: {:?}", n_points, @@ -727,7 +729,7 @@ mod tests { // Get all boundary facets and verify they are correctly identified let mut boundary_count = 0; - for boundary_facet in dt.boundary_facets() { + for boundary_facet in dt.boundary_facets().unwrap() { let is_boundary = dt .tds() .is_boundary_facet_with_map(&boundary_facet, &facet_to_simplices) @@ -747,7 +749,7 @@ mod tests { ); // Verify consistency - let reported_count = dt.boundary_facets().count(); + let reported_count = dt.boundary_facets().unwrap().count(); assert_eq!( boundary_count, reported_count, "Boundary facet count should be consistent" @@ -767,7 +769,7 @@ mod tests { ]; let vertices = Vertex::from_points(&points); let dt = DelaunayTriangulation::new(&vertices).unwrap(); - let facet = dt.boundary_facets().next().unwrap(); + let facet = dt.boundary_facets().unwrap().next().unwrap(); let facet_key = facet.key().unwrap(); let mut facet_to_simplices = dt.tds().build_facet_to_simplices_map().unwrap(); @@ -827,12 +829,6 @@ mod tests { #[test] fn test_boundary_facets_error_propagation_from_build_map() { - println!("Testing error propagation from build_facet_to_simplices_map"); - - // Test that boundary_facets properly propagates errors from build_facet_to_simplices_map - // This exercises the error propagation path in boundary_facets() - - // Create a minimal valid triangulation let points = vec![ Point::new([0.0, 0.0, 0.0]), Point::new([1.0, 0.0, 0.0]), @@ -840,23 +836,25 @@ mod tests { Point::new([0.0, 0.0, 1.0]), ]; let vertices = Vertex::from_points(&points); - let dt = DelaunayTriangulation::new(&vertices).unwrap(); + let mut dt: DelaunayTriangulation<_, (), (), 3> = + DelaunayTriangulation::new(&vertices).unwrap(); + let (simplex_key, _) = dt.tri.tds.simplices().next().unwrap(); + let first_vertex = dt.tri.tds.simplex(simplex_key).unwrap().vertices()[0]; - // Test that build_facet_to_simplices_map succeeds on valid triangulation - let map_result = dt.tds().build_facet_to_simplices_map(); - assert!( - map_result.is_ok(), - "build_facet_to_simplices_map should succeed on valid TDS" - ); - - // Test that boundary_facets succeeds when build_facet_to_simplices_map succeeds - let boundary_count = dt.boundary_facets().count(); - assert_eq!( - boundary_count, 4, - "Single tetrahedron should have 4 boundary facets" - ); + { + let simplex = dt.tri.tds.simplex_mut(simplex_key).unwrap(); + while simplex.number_of_vertices() <= usize::from(u8::MAX) + 1 { + simplex.push_vertex_key(first_vertex); + } + } - println!(" ✓ Error propagation path from build_facet_to_simplices_map verified"); + match dt.boundary_facets() { + Ok(_) => panic!("corrupted facet map should return a query error"), + Err(QueryError::TriangulationCorrupted { + source: TdsError::IndexOutOfBounds { .. }, + }) => {} + Err(err) => panic!("expected index-out-of-bounds query error, got {err:?}"), + } } #[test] @@ -876,7 +874,7 @@ mod tests { let dt = DelaunayTriangulation::new(&vertices).unwrap(); // Test both methods return consistent results - let boundary_facets_count = dt.boundary_facets().count(); + let boundary_facets_count = dt.boundary_facets().unwrap().count(); let boundary_count = dt .tds() .number_of_boundary_facets() diff --git a/src/core/construction.rs b/src/core/construction.rs index 8b0eb0b6..54fc1a32 100644 --- a/src/core/construction.rs +++ b/src/core/construction.rs @@ -131,6 +131,17 @@ pub enum TriangulationConstructionError { source: TriangulationValidationError, }, + /// Local facet repair would remove more simplices than the active budget allowed. + #[error( + "Local facet repair removal budget exceeded during construction: would remove {attempted} simplices, maximum is {max_simplices_removed}" + )] + LocalRepairBudgetExceeded { + /// Maximum simplices the repair budget allowed for removal. + max_simplices_removed: usize, + /// Number of simplices selected for removal. + attempted: usize, + }, + /// Final cumulative topology validation failed after construction. /// /// Mirrors [`InsertionTopologyValidation`](Self::InsertionTopologyValidation) @@ -177,7 +188,8 @@ where /// with explicit boundary neighbor slots. The simplex is validated to /// ensure it is non-degenerate (vertices span full D-dimensional space). /// - /// **Design Note**: This method uses [`robust_orientation`] directly for + /// **Design Note**: This method uses [`robust_orientation`] directly \[1] + /// (Shewchuk robust predicates; see `REFERENCES.md`) for /// the non-degeneracy check, bypassing the kernel. This avoids `SoS` /// tie-breaking that would mask truly degenerate input and keeps the /// method independent of kernel state. diff --git a/src/core/orientation.rs b/src/core/orientation.rs index f48111c5..42a2f937 100644 --- a/src/core/orientation.rs +++ b/src/core/orientation.rs @@ -10,6 +10,7 @@ use crate::core::collections::{MAX_PRACTICAL_DIMENSION_SIZE, SimplexKeyBuffer, S use crate::core::simplex::Simplex; use crate::core::tds::{GeometricError, SimplexKey, TdsError, VertexKey}; use crate::core::triangulation::Triangulation; +use crate::core::validation::TriangulationValidationError; use crate::geometry::kernel::Kernel; use crate::geometry::point::Point; use crate::geometry::predicates::Orientation; @@ -342,13 +343,13 @@ where } } let sampled: Vec = sample_keys.into_iter().flatten().collect(); - tracing::debug!( - residual_count, - sampled_keys = ?sampled, - "normalize_and_promote_positive_orientation: \ - {residual_count} simplices still appear negative after bounded promotion \ - passes (likely near-degenerate FP noise); accepting coherent orientation" - ); + return Err(InsertionError::TopologyValidationFailed { + message: "Positive-orientation promotion failed to converge".to_string(), + source: TriangulationValidationError::OrientationPromotionNonConvergence { + residual_count, + sampled, + }, + }); } self.canonicalize_global_orientation_sign()?; Ok(()) diff --git a/src/core/query.rs b/src/core/query.rs index eeb59df1..48e9bcc7 100644 --- a/src/core/query.rs +++ b/src/core/query.rs @@ -12,7 +12,7 @@ use crate::core::collections::{ use crate::core::edge::EdgeKey; use crate::core::facet::{AllFacetsIter, BoundaryFacetsIter}; use crate::core::simplex::Simplex; -use crate::core::tds::{SimplexKey, VertexKey}; +use crate::core::tds::{SimplexKey, TdsError, VertexKey}; use crate::core::triangulation::Triangulation; use crate::core::vertex::Vertex; use crate::geometry::kernel::Kernel; @@ -22,6 +22,40 @@ use std::sync::atomic::{AtomicU64, Ordering}; #[cfg(debug_assertions)] static VERTEX_TO_SIMPLICES_SPILL_EVENTS: AtomicU64 = AtomicU64::new(0); +/// Errors returned by read-only triangulation queries. +/// +/// These errors indicate that a supposedly read-only query could not derive +/// its auxiliary topology view because the underlying triangulation state is +/// inconsistent. +/// +/// # Examples +/// +/// ```rust +/// use delaunay::prelude::*; +/// +/// 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::new(&vertices).unwrap(); +/// +/// let boundary_count = dt.as_triangulation().boundary_facets().unwrap().count(); +/// assert_eq!(boundary_count, 4); +/// ``` +#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum QueryError { + /// The triangulation could not build a facet map for a read-only query. + #[error("Triangulation data structure is corrupted: {source}")] + TriangulationCorrupted { + /// Typed TDS validation or bookkeeping error that prevented the query. + #[from] + source: TdsError, + }, +} + impl Triangulation where K: Kernel, @@ -190,12 +224,6 @@ where /// /// An iterator yielding `FacetView` objects for boundary facets only. /// - /// # Panics - /// - /// Panics if the triangulation data structure is corrupted (simplices have invalid - /// neighbor relationships or facet information). This indicates a bug in the - /// library and should never happen with a properly constructed triangulation. - /// /// # Examples /// /// ```rust @@ -209,17 +237,23 @@ where /// ]; /// let dt = DelaunayTriangulation::new(&vertices).unwrap(); /// - /// let boundary_count = dt.as_triangulation().boundary_facets().count(); + /// let boundary_count = dt.as_triangulation().boundary_facets().unwrap().count(); /// assert_eq!(boundary_count, 4); // All facets are on boundary /// ``` - pub fn boundary_facets(&self) -> BoundaryFacetsIter<'_, K::Scalar, U, V, D> { - // build_facet_to_simplices_map only fails if simplices have invalid structure, - // which should never happen in a valid triangulation + /// + /// # Errors + /// + /// Returns [`QueryError::TriangulationCorrupted`] if facet-map construction + /// detects invalid simplex or facet bookkeeping. The variant preserves the + /// underlying [`TdsError`] so callers can inspect the structural failure. + pub fn boundary_facets( + &self, + ) -> Result, QueryError> { let facet_map = self .tds .build_facet_to_simplices_map() - .expect("Failed to build facet map - triangulation structure is corrupted"); - BoundaryFacetsIter::new(&self.tds, facet_map) + .map_err(|source| QueryError::TriangulationCorrupted { source })?; + Ok(BoundaryFacetsIter::new(&self.tds, facet_map)) } #[inline] @@ -659,7 +693,7 @@ mod tests { assert_eq!(empty.simplices().count(), 0); assert_eq!(empty.vertices().count(), 0); assert_eq!(empty.facets().count(), 0); - assert_eq!(empty.boundary_facets().count(), 0); + assert_eq!(empty.boundary_facets().unwrap().count(), 0); let vertices = vec![ $(vertex!($simplex_coords)),+ @@ -680,7 +714,7 @@ mod tests { assert_eq!(tri.simplices().count(), 1); assert_eq!(tri.vertices().count(), expected_vertex_count); assert_eq!(tri.facets().count(), expected_vertex_count); - assert_eq!(tri.boundary_facets().count(), expected_vertex_count); + assert_eq!(tri.boundary_facets().unwrap().count(), expected_vertex_count); } } }; @@ -718,6 +752,36 @@ mod tests { ] ); + #[test] + fn test_boundary_facets_reports_corrupted_facet_map() { + let vertices = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut tds = + Triangulation::, (), (), 2>::build_initial_simplex(&vertices).unwrap(); + let (simplex_key, _) = tds.simplices().next().unwrap(); + let first_vertex = tds.simplex(simplex_key).unwrap().vertices()[0]; + + { + let simplex = tds.simplex_mut(simplex_key).unwrap(); + while simplex.number_of_vertices() <= usize::from(u8::MAX) + 1 { + simplex.push_vertex_key(first_vertex); + } + } + + let tri = Triangulation::, (), (), 2>::new_with_tds(FastKernel::new(), tds); + + match tri.boundary_facets() { + Ok(_) => panic!("corrupted facet map should return a query error"), + Err(QueryError::TriangulationCorrupted { + source: TdsError::IndexOutOfBounds { .. }, + }) => {} + Err(err) => panic!("expected index-out-of-bounds query error, got {err:?}"), + } + } + #[test] fn topology_edges_triangle_2d() { let vertices = vec![ @@ -1095,7 +1159,7 @@ mod tests { assert!(edge_count >= 6); assert!(tri.facets().next().is_some()); - assert!(tri.boundary_facets().next().is_some()); + assert!(tri.boundary_facets().unwrap().next().is_some()); let (simplex_key, _) = tri.simplices().next().unwrap(); let simplex_vertices = tri.simplex_vertices(simplex_key).unwrap(); diff --git a/src/core/repair.rs b/src/core/repair.rs index e2a01c0a..35e2e1ff 100644 --- a/src/core/repair.rs +++ b/src/core/repair.rs @@ -583,7 +583,7 @@ where /// assert!(issues.is_none()); /// /// // Note: This method is most useful for checking newly created simplices - /// // after insertion/removal operations (see usage in insert_transactional). + /// // after insertion/removal operations. /// ``` pub fn detect_local_facet_issues( &self, @@ -786,6 +786,7 @@ where /// # Arguments /// /// * `issues` - Detected facet issues map from `detect_local_facet_issues()` + /// * `max_simplices_removed` - Maximum simplices this repair may remove /// /// # Returns /// @@ -799,7 +800,10 @@ where /// /// Returns an [`InsertionError`] if quality evaluation, facet bookkeeping, /// neighbor repair, incident-simplex assignment, or final topology - /// validation fails. + /// validation fails. Returns + /// [`InsertionError::MaxSimplicesRemovedExceeded`] when the selected repair + /// would remove more simplices than `max_simplices_removed` allows; in that + /// case the original TDS is restored before returning the error. /// /// # Examples /// @@ -827,7 +831,7 @@ where /// /// // Empty issues map => nothing to remove. /// let mut tri = dt.as_triangulation().clone(); - /// let removed = tri.repair_local_facet_issues(&FacetIssuesMap::default())?; + /// let removed = tri.repair_local_facet_issues(&FacetIssuesMap::default(), 0)?; /// assert_eq!(removed, 0); /// # Ok(()) /// # } @@ -835,10 +839,11 @@ where /// /// In practice, this method is typically called with issues detected by /// [`detect_local_facet_issues`](Self::detect_local_facet_issues) after insertion/removal - /// operations. See `insert_transactional` for a typical usage pattern. + /// operations. pub fn repair_local_facet_issues( &mut self, issues: &FacetIssuesMap, + max_simplices_removed: usize, ) -> Result where K::Scalar: Div, @@ -848,6 +853,12 @@ where let outcome = self .repair_local_facet_issues_with_frontier(issues) .map_err(InsertionError::TopologyValidation)?; + if outcome.removed_count > max_simplices_removed { + return Err(InsertionError::MaxSimplicesRemovedExceeded { + max_simplices_removed, + attempted: outcome.removed_count, + }); + } if outcome.removed_count == 0 { return Ok(0); } @@ -1034,7 +1045,7 @@ mod tests { // Empty issues map: should remove nothing let empty_issues = FacetIssuesMap::default(); - let removed = tri.repair_local_facet_issues(&empty_issues).unwrap(); + let removed = tri.repair_local_facet_issues(&empty_issues, 0).unwrap(); assert_eq!(removed, 0, "{}D: Empty issues should remove 0 simplices", $dim); assert_eq!(tri.tds.number_of_simplices(), 1, "{}D: Should still have 1 simplex", $dim); } @@ -1485,7 +1496,7 @@ mod tests { assert!(issues.is_some(), "Should detect over-shared facet"); let original_simplex_count = tri.tds.number_of_simplices(); - match tri.repair_local_facet_issues(&issues.unwrap()) { + match tri.repair_local_facet_issues(&issues.unwrap(), usize::MAX) { Ok(removed) => { assert!(removed > 0, "repair should remove at least one simplex"); tri.validate() @@ -1601,7 +1612,7 @@ mod tests { let original_simplex_count = tri.tds.number_of_simplices(); let original_vertex_count = tri.tds.number_of_vertices(); - let result = tri.repair_local_facet_issues(&issues); + let result = tri.repair_local_facet_issues(&issues, usize::MAX); assert!( result.is_err(), @@ -1616,4 +1627,25 @@ mod tests { ); } } + + #[test] + fn test_repair_local_facet_issues_respects_removal_budget() { + let (mut tri, original_simplices, _, _) = build_overshared_edge_fixture(); + let issues = tri + .detect_local_facet_issues(&original_simplices) + .unwrap() + .expect("three simplices sharing one edge should be detected as over-shared"); + let original_simplex_count = tri.tds.number_of_simplices(); + + let result = tri.repair_local_facet_issues(&issues, 0); + + assert!(matches!( + result, + Err(InsertionError::MaxSimplicesRemovedExceeded { + max_simplices_removed: 0, + attempted + }) if attempted > 0 + )); + assert_eq!(tri.tds.number_of_simplices(), original_simplex_count); + } } diff --git a/src/core/tds.rs b/src/core/tds.rs index a903cd88..66f6d9a2 100644 --- a/src/core/tds.rs +++ b/src/core/tds.rs @@ -1230,6 +1230,8 @@ pub enum TriangulationValidationErrorKind { IsolatedVertex, /// The simplex-neighbor graph was disconnected. Disconnected, + /// Positive-orientation promotion did not converge. + OrientationPromotionNonConvergence, } impl From<&TriangulationValidationError> for TriangulationValidationErrorKind { @@ -1250,6 +1252,9 @@ impl From<&TriangulationValidationError> for TriangulationValidationErrorKind { } TriangulationValidationError::IsolatedVertex { .. } => Self::IsolatedVertex, TriangulationValidationError::Disconnected { .. } => Self::Disconnected, + TriangulationValidationError::OrientationPromotionNonConvergence { .. } => { + Self::OrientationPromotionNonConvergence + } } } } @@ -6899,6 +6904,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::DelaunayTriangulation; use crate::builder::DelaunayTriangulationBuilder; use crate::core::algorithms::flips::DelaunayRepairError; use crate::core::algorithms::incremental_insertion::InsertionError; @@ -6912,7 +6918,6 @@ mod tests { use crate::geometry::traits::coordinate::Coordinate; use crate::repair::DelaunayRepairOperation; use crate::topology::characteristics::euler::TopologyClassification; - use crate::triangulation::DelaunayTriangulation; use crate::validation::DelaunayTriangulationValidationError; use crate::vertex; use slotmap::KeyData; diff --git a/src/core/traits/boundary_analysis.rs b/src/core/traits/boundary_analysis.rs index 6b80cbbd..657fd928 100644 --- a/src/core/traits/boundary_analysis.rs +++ b/src/core/traits/boundary_analysis.rs @@ -202,7 +202,7 @@ pub trait BoundaryAnalysis { /// assert_eq!(tds.number_of_boundary_facets().unwrap(), 4); /// /// // Alternative: using iterator (useful for additional processing) - /// let count_via_iter = dt.boundary_facets().count(); + /// let count_via_iter = dt.boundary_facets().unwrap().count(); /// assert_eq!(count_via_iter, 4); /// ``` fn number_of_boundary_facets(&self) -> Result; diff --git a/src/core/validation.rs b/src/core/validation.rs index 850d5c97..01084d98 100644 --- a/src/core/validation.rs +++ b/src/core/validation.rs @@ -238,6 +238,21 @@ pub enum TriangulationValidationError { classification: TopologyClassification, }, + /// Positive-orientation promotion did not converge within its bounded pass budget. + /// + /// This is reported at the triangulation layer because the TDS may still be + /// structurally coherent while failing the geometric simplex-orientation + /// invariant expected by triangulation validation. + #[error( + "Positive-orientation promotion did not converge: {residual_count} residual negative-orientation simplices, sampled keys {sampled:?}" + )] + OrientationPromotionNonConvergence { + /// Number of simplices still evaluated as negative after promotion. + residual_count: usize, + /// Sample of residual simplex keys for diagnostics. + sampled: Vec, + }, + /// Vertex is not incident to any simplex. /// /// An isolated vertex violates manifold invariants at the topology (Level 3) layer diff --git a/src/delaunay/builder.rs b/src/delaunay/builder.rs index 9e7a7688..32324e72 100644 --- a/src/delaunay/builder.rs +++ b/src/delaunay/builder.rs @@ -458,6 +458,8 @@ pub enum ExplicitInsertionErrorKind { TopologyValidation, /// Triangulation-layer topology validation failed. TopologyValidationFailed, + /// Local repair would exceed its simplex-removal budget. + MaxSimplicesRemovedExceeded, } /// Compact summary of an [`InsertionError`] used by explicit construction. @@ -521,6 +523,9 @@ impl From for ExplicitInsertionError { InsertionError::TopologyValidationFailed { .. } => { ExplicitInsertionErrorKind::TopologyValidationFailed } + InsertionError::MaxSimplicesRemovedExceeded { .. } => { + ExplicitInsertionErrorKind::MaxSimplicesRemovedExceeded + } }; let source_kind = match &source { InsertionError::DelaunayValidationFailed { source } => { diff --git a/src/delaunay/construction.rs b/src/delaunay/construction.rs index c37d4710..0cf8f8f8 100644 --- a/src/delaunay/construction.rs +++ b/src/delaunay/construction.rs @@ -344,6 +344,17 @@ pub enum DelaunayConstructionFailure { message: String, }, + /// Local facet repair would remove more simplices than the active budget allowed. + #[error( + "local facet repair removal budget exceeded during construction: attempted {attempted}, max {max_simplices_removed}" + )] + LocalRepairBudgetExceeded { + /// Maximum simplices the repair budget allowed for removal. + max_simplices_removed: usize, + /// Number of simplices selected for removal. + attempted: usize, + }, + /// Final topology validation failed after construction. #[error("final topology validation failed after construction: {message}: {source}")] FinalTopologyValidation { @@ -409,6 +420,13 @@ impl From for DelaunayConstructionFailure { message: format!("{message}: {source}"), } } + TriangulationConstructionError::LocalRepairBudgetExceeded { + max_simplices_removed, + attempted, + } => Self::LocalRepairBudgetExceeded { + max_simplices_removed, + attempted, + }, TriangulationConstructionError::FinalTopologyValidation { message, source } => { Self::FinalTopologyValidation { message, source } } @@ -4252,6 +4270,7 @@ where } | DelaunayConstructionFailure::InternalInconsistency { .. } | DelaunayConstructionFailure::DelaunayRepair { .. } | DelaunayConstructionFailure::InsertionTopologyValidation { .. } + | DelaunayConstructionFailure::LocalRepairBudgetExceeded { .. } | DelaunayConstructionFailure::FinalTopologyValidation { .. }, ) ) @@ -4349,6 +4368,13 @@ where ), } } + InsertionError::MaxSimplicesRemovedExceeded { + max_simplices_removed, + attempted, + } => TriangulationConstructionError::LocalRepairBudgetExceeded { + max_simplices_removed, + attempted, + }, InsertionError::DelaunayRepairFailed { source, context } => { let message = format!( "Failed to canonicalize orientation after post-construction repair: \ @@ -4431,6 +4457,13 @@ where InsertionError::TopologyValidationFailed { message, source } => { TriangulationConstructionError::InsertionTopologyValidation { message, source } } + InsertionError::MaxSimplicesRemovedExceeded { + max_simplices_removed, + attempted, + } => TriangulationConstructionError::LocalRepairBudgetExceeded { + max_simplices_removed, + attempted, + }, } } @@ -6509,6 +6542,22 @@ mod tests { )); } + #[test] + fn test_map_orientation_canonicalization_error_preserves_repair_budget() { + let error = InsertionError::MaxSimplicesRemovedExceeded { + max_simplices_removed: 2, + attempted: 3, + }; + let mapped = TestDelaunay::<3>::map_orientation_canonicalization_error(error); + assert!(matches!( + mapped, + TriangulationConstructionError::LocalRepairBudgetExceeded { + max_simplices_removed: 2, + attempted: 3, + } + )); + } + #[test] fn test_map_orientation_canonicalization_error_duplicate_uuid_is_internal() { let error = InsertionError::DuplicateUuid { @@ -6752,6 +6801,33 @@ mod tests { ); } + #[test] + fn test_map_insertion_error_preserves_repair_budget() { + let error = InsertionError::MaxSimplicesRemovedExceeded { + max_simplices_removed: 2, + attempted: 3, + }; + let mapped = TestDelaunay::<3>::map_insertion_error(error); + assert!(matches!( + mapped, + TriangulationConstructionError::LocalRepairBudgetExceeded { + max_simplices_removed: 2, + attempted: 3, + } + )); + + let public_error: DelaunayTriangulationConstructionError = mapped.into(); + assert!(matches!( + public_error, + DelaunayTriangulationConstructionError::Triangulation( + DelaunayConstructionFailure::LocalRepairBudgetExceeded { + max_simplices_removed: 2, + attempted: 3, + } + ) + )); + } + #[test] fn test_map_orientation_canonicalization_error_orientation_violation_is_internal_inconsistency() { @@ -6820,6 +6896,20 @@ mod tests { ); } + #[test] + fn test_is_non_retryable_construction_error_repair_budget() { + let err: DelaunayTriangulationConstructionError = + TriangulationConstructionError::LocalRepairBudgetExceeded { + max_simplices_removed: 2, + attempted: 3, + } + .into(); + assert!( + TestDelaunay::<3>::is_non_retryable_construction_error(&err), + "Local repair budget exhaustion should be non-retryable" + ); + } + #[test] fn test_is_non_retryable_construction_error_tds_validation() { let err: DelaunayTriangulationConstructionError = TriangulationConstructionError::Tds( diff --git a/src/delaunay/insertion.rs b/src/delaunay/insertion.rs index 4e7450f7..453b727a 100644 --- a/src/delaunay/insertion.rs +++ b/src/delaunay/insertion.rs @@ -3,6 +3,16 @@ //! This module owns post-construction mutation APIs: inserting vertices, removing //! vertices, maintaining insertion caches, and running policy-controlled local //! Delaunay repair after those mutations. +//! +//! The insertion workflow follows the Bowyer-Watson cavity model \[2]\[3]: +//! point location and cavity identification depend on exact orientation and +//! in-sphere predicates \[1], then the TDS fills the cavity and locally rewires +//! neighbors. Flip-based repair uses regular-triangulation flip theory \[4]. +//! Robust fallback rebuilds and fan retriangulation are bounded recovery paths: +//! they still validate with exact predicates where they identify cavities or +//! test in-sphere/Delaunay violations, but their local repair ordering is +//! heuristic and must be followed by topology and Delaunay validation \[5]. +//! See `REFERENCES.md` for the numbered bibliography. #![forbid(unsafe_code)] @@ -828,9 +838,12 @@ mod tests { source.as_ref(), DelaunayRepairError::NonConvergent { max_flips: 0, .. } ) => {} - InvariantError::Tds(TdsError::FacetSharingViolation { .. }) => {} + InvariantError::Triangulation( + TriangulationValidationError::OrientationPromotionNonConvergence { .. }, + ) + | InvariantError::Tds(TdsError::FacetSharingViolation { .. }) => {} other => panic!( - "expected vertex-removal RepairOperationFailed from forced repair path, got {other:?}" + "expected vertex-removal rollback error from forced repair path, got {other:?}" ), } diff --git a/src/delaunay/query.rs b/src/delaunay/query.rs index 14846440..bc6e7830 100644 --- a/src/delaunay/query.rs +++ b/src/delaunay/query.rs @@ -10,6 +10,7 @@ use crate::core::adjacency::{AdjacencyIndex, AdjacencyIndexBuildError}; use crate::core::edge::EdgeKey; use crate::core::facet::{AllFacetsIter, BoundaryFacetsIter}; +use crate::core::query::QueryError; use crate::core::simplex::Simplex; use crate::core::tds::{SimplexKey, Tds, VertexKey}; use crate::core::traits::data_type::DataType; @@ -350,6 +351,43 @@ where &self.tri } + /// Returns an iterator over boundary (hull) facets in the triangulation. + /// + /// Boundary facets are those that belong to exactly one simplex. This method + /// computes the facet-to-simplices map internally for convenience. + /// + /// # Returns + /// + /// An iterator yielding `FacetView` objects for boundary facets only. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// + /// 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::new(&vertices).unwrap(); + /// + /// let boundary_count = dt.boundary_facets().unwrap().count(); + /// assert_eq!(boundary_count, 4); // All facets are on boundary + /// ``` + /// + /// # Errors + /// + /// Returns [`QueryError::TriangulationCorrupted`] if facet-map construction + /// detects invalid simplex or facet bookkeeping. The variant preserves the + /// lower-level [`TdsError`](crate::tds::TdsError) for diagnostics. + pub fn boundary_facets( + &self, + ) -> Result, QueryError> { + self.tri.boundary_facets() + } + /// Returns the insertion-time global topology validation policy used by the underlying /// triangulation. /// @@ -419,6 +457,19 @@ where self.tri.set_validation_policy(policy); } /// Returns the automatic Delaunay repair policy. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::repair::DelaunayRepairPolicy; + /// + /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; + /// let dt: DelaunayTriangulation<_, (), (), 2> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// assert_eq!(dt.delaunay_repair_policy(), DelaunayRepairPolicy::EveryInsertion); + /// ``` #[inline] #[must_use] pub const fn delaunay_repair_policy(&self) -> DelaunayRepairPolicy { @@ -426,12 +477,42 @@ where } /// Sets the automatic Delaunay repair policy. + /// + /// This affects future incremental insertions; it does not rewrite already + /// stored topology. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::repair::DelaunayRepairPolicy; + /// + /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; + /// let mut dt: DelaunayTriangulation<_, (), (), 2> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); + /// assert_eq!(dt.delaunay_repair_policy(), DelaunayRepairPolicy::Never); + /// ``` #[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::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::repair::DelaunayCheckPolicy; + /// + /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; + /// let dt: DelaunayTriangulation<_, (), (), 2> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// + /// assert_eq!(dt.delaunay_check_policy(), DelaunayCheckPolicy::EndOnly); + /// ``` #[inline] #[must_use] pub const fn delaunay_check_policy(&self) -> DelaunayCheckPolicy { @@ -439,6 +520,25 @@ where } /// Sets the automatic global Delaunay validation policy. + /// + /// This affects future incremental insertions; it does not perform an + /// immediate global Delaunay check. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; + /// use delaunay::prelude::repair::DelaunayCheckPolicy; + /// use std::num::NonZeroUsize; + /// + /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; + /// let mut dt: DelaunayTriangulation<_, (), (), 2> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// let every_two = NonZeroUsize::new(2).unwrap(); + /// + /// dt.set_delaunay_check_policy(DelaunayCheckPolicy::EveryN(every_two)); + /// assert_eq!(dt.delaunay_check_policy(), DelaunayCheckPolicy::EveryN(every_two)); + /// ``` #[inline] pub const fn set_delaunay_check_policy(&mut self, policy: DelaunayCheckPolicy) { self.insertion_state.delaunay_check_policy = policy; @@ -583,35 +683,6 @@ where self.tri.facets() } - /// Returns an iterator over boundary (hull) facets in the triangulation. - /// - /// Boundary facets are those that belong to exactly one simplex. This method - /// computes the facet-to-simplices map internally for convenience. - /// - /// # Returns - /// - /// An iterator yielding `FacetView` objects for boundary facets only. - /// - /// # Examples - /// - /// ```rust - /// use delaunay::prelude::construction::{DelaunayTriangulation, vertex}; - /// - /// 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::new(&vertices).unwrap(); - /// - /// let boundary_count = dt.boundary_facets().count(); - /// assert_eq!(boundary_count, 4); // All facets are on boundary - /// ``` - pub fn boundary_facets(&self) -> BoundaryFacetsIter<'_, K::Scalar, U, V, D> { - self.tri.boundary_facets() - } - /// Builds an immutable adjacency index for fast repeated topology queries. /// /// This is a convenience wrapper around @@ -896,10 +967,14 @@ where #[cfg(test)] mod tests { use super::*; + use crate::core::operations::DelaunayInsertionState; + use crate::core::tds::TdsError; use crate::geometry::kernel::{AdaptiveKernel, FastKernel}; use crate::vertex; use std::{collections::HashSet, num::NonZeroUsize, sync::Once}; + struct Payload; + fn init_tracing() { static INIT: Once = Once::new(); INIT.call_once(|| { @@ -968,6 +1043,47 @@ mod tests { assert_eq!(dt.delaunay_check_policy(), policy); } + #[test] + fn test_boundary_facets_propagates_core_query_error() { + init_tracing(); + let vertices: Vec> = vec![ + vertex!([0.0, 0.0]), + vertex!([1.0, 0.0]), + vertex!([0.0, 1.0]), + ]; + let mut dt: DelaunayTriangulation<_, (), (), 2> = + DelaunayTriangulation::new(&vertices).unwrap(); + let (simplex_key, _) = dt.tri.tds.simplices().next().unwrap(); + let first_vertex = dt.tri.tds.simplex(simplex_key).unwrap().vertices()[0]; + + { + let simplex = dt.tri.tds.simplex_mut(simplex_key).unwrap(); + while simplex.number_of_vertices() <= usize::from(u8::MAX) + 1 { + simplex.push_vertex_key(first_vertex); + } + } + + match dt.boundary_facets() { + Ok(_) => panic!("corrupted facet map should return a query error"), + Err(QueryError::TriangulationCorrupted { + source: TdsError::IndexOutOfBounds { .. }, + }) => {} + Err(err) => panic!("expected index-out-of-bounds query error, got {err:?}"), + } + } + + #[test] + fn test_boundary_facets_accepts_non_datatype_payloads() { + let dt: DelaunayTriangulation, Payload, Payload, 2> = + DelaunayTriangulation { + tri: Triangulation::new_empty(FastKernel::new()), + insertion_state: DelaunayInsertionState::new(), + spatial_index: None, + }; + + assert_eq!(dt.boundary_facets().unwrap().count(), 0); + } + #[test] fn test_validation_policy_defaults_to_on_suspicion() { init_tracing(); diff --git a/src/delaunay/repair.rs b/src/delaunay/repair.rs index 0a24fba2..8e9221d7 100644 --- a/src/delaunay/repair.rs +++ b/src/delaunay/repair.rs @@ -37,6 +37,7 @@ use std::{ // Heuristic rebuild attempts must be consistent across build profiles to avoid // release-only construction failures (see #306). 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) }; @@ -49,13 +50,22 @@ struct HeuristicRebuildRecursionGuard { impl HeuristicRebuildRecursionGuard { /// Tracks nested heuristic rebuilds so fallback construction cannot recurse /// indefinitely through repair hooks. - fn enter() -> Self { + fn enter() -> Result { let prior_depth = HEURISTIC_REBUILD_DEPTH.with(|depth| { let prior = depth.get(); - depth.set(prior.saturating_add(1)); + if prior < MAX_HEURISTIC_REBUILD_DEPTH { + depth.set(prior.saturating_add(1)); + } prior }); - Self { prior_depth } + if prior_depth >= MAX_HEURISTIC_REBUILD_DEPTH { + return Err(DelaunayRepairError::HeuristicRebuildFailed { + message: format!( + "heuristic rebuild recursion depth exceeded {MAX_HEURISTIC_REBUILD_DEPTH}" + ), + }); + } + Ok(Self { prior_depth }) } } @@ -673,7 +683,7 @@ where }; let rebuild_attempt = (|| { - let _guard = HeuristicRebuildRecursionGuard::enter(); + let _guard = HeuristicRebuildRecursionGuard::enter()?; // Shuffle vertices for this attempt. let mut vertices = base_vertices.clone(); @@ -692,10 +702,8 @@ where ); candidate.set_global_topology(global_topology); - // During rebuild, force local repair after every insertion. We'll restore the caller's - // policies after we have a repaired candidate. - let rebuild_repair_policy = candidate.insertion_state.delaunay_repair_policy; - let rebuild_check_policy = candidate.insertion_state.delaunay_check_policy; + // During rebuild, force local repair after every insertion. The caller's + // policies are copied onto the finished candidate below. candidate.insertion_state.delaunay_repair_policy = DelaunayRepairPolicy::EveryInsertion; candidate.insertion_state.delaunay_check_policy = DelaunayCheckPolicy::EndOnly; @@ -775,9 +783,6 @@ where self.insertion_state.delaunay_repair_insertion_count; candidate.insertion_state.last_inserted_simplex = None; - // Restore prior rebuild-only policies (kept for completeness; currently overwritten above). - let _ = (rebuild_repair_policy, rebuild_check_policy); - let topology = candidate.tri.topology_guarantee(); candidate.invalidate_locate_hint_cache(); let (tds, kernel) = (&mut candidate.tri.tds, &candidate.tri.kernel); diff --git a/src/delaunay/validation.rs b/src/delaunay/validation.rs index 66f62624..9574b928 100644 --- a/src/delaunay/validation.rs +++ b/src/delaunay/validation.rs @@ -187,6 +187,9 @@ impl ValidationCadence { if let Some(every) = NonZeroUsize::new(every) { Self::EveryN(every) } else { + // Logically unreachable because `Some(0)` is matched above. + // Keep this branch so the function remains const without + // introducing unsafe code for `NonZeroUsize::new_unchecked`. Self::Never } } diff --git a/src/lib.rs b/src/lib.rs index a38ee5f8..ab5508d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1213,6 +1213,7 @@ pub mod algorithms { /// ``` pub mod query { pub use crate::assert_jaccard_gte; + pub use crate::core::query::QueryError; pub use crate::core::traits::boundary_analysis::BoundaryAnalysis; pub use crate::core::traits::data_type::{ DataCopy, DataDebug, DataDeserialize, DataIdentity, DataSerde, DataSerialize, DataType, @@ -1244,7 +1245,7 @@ pub mod prelude { // Re-export the public low-level facades. pub use crate::query::{ BoundaryAnalysis, DataCopy, DataDebug, DataDeserialize, DataIdentity, DataSerde, - DataSerialize, DataType, + DataSerialize, DataType, QueryError, }; pub use crate::tds::*; pub use crate::{ @@ -1414,6 +1415,7 @@ pub mod prelude { pub use crate::query::{ AdjacencyIndex, AdjacencyIndexBuildError, BoundaryAnalysis, DataCopy, DataDebug, DataDeserialize, DataIdentity, DataSerde, DataSerialize, DataType, EdgeKey, FacetView, + QueryError, }; pub use crate::tds::{ FacetHandle, InvariantError, InvariantErrorSummary, InvariantErrorSummaryDetail, @@ -1739,7 +1741,7 @@ pub mod prelude { pub use crate::geometry::traits::coordinate::Coordinate; pub use crate::query::{ BoundaryAnalysis, DataCopy, DataDebug, DataDeserialize, DataIdentity, DataSerde, - DataSerialize, DataType, FacetView, Simplex, Vertex, + DataSerialize, DataType, FacetView, QueryError, Simplex, Vertex, }; // Read-only predicates (useful in benchmarks / lightweight geometry checks) diff --git a/tests/example_workflows.rs b/tests/example_workflows.rs index d2bfc29a..fc1c2af2 100644 --- a/tests/example_workflows.rs +++ b/tests/example_workflows.rs @@ -5,7 +5,7 @@ use delaunay::prelude::construction::{ DelaunayTriangulation, DelaunayTriangulationConstructionError, vertex, }; -use delaunay::prelude::query::{ConvexHull, Coordinate, Point}; +use delaunay::prelude::query::{ConvexHull, Coordinate, Point, QueryError}; #[test] fn triangulation_and_hull_workflow_remains_valid() -> Result<(), WorkflowTestError> { @@ -24,7 +24,7 @@ fn triangulation_and_hull_workflow_remains_valid() -> Result<(), WorkflowTestErr let index = dt.build_adjacency_index()?; assert!(index.number_of_edges() > 0); - let boundary_facets: Vec<_> = dt.boundary_facets().collect(); + let boundary_facets: Vec<_> = dt.boundary_facets()?.collect(); assert!(!boundary_facets.is_empty()); let hull = ConvexHull::from_triangulation(dt.as_triangulation())?; @@ -57,6 +57,8 @@ enum WorkflowTestError { Validation(#[from] delaunay::prelude::validation::DelaunayTriangulationValidationError), #[error(transparent)] AdjacencyIndex(#[from] delaunay::prelude::query::AdjacencyIndexBuildError), + #[error(transparent)] + Query(#[from] QueryError), #[error("convex hull construction failed: {source}")] ConvexHullConstruction { #[source] diff --git a/tests/prelude_exports.rs b/tests/prelude_exports.rs index c4d9b6f0..c1d96d84 100644 --- a/tests/prelude_exports.rs +++ b/tests/prelude_exports.rs @@ -51,7 +51,7 @@ use delaunay::prelude::ordering::{ HilbertError, hilbert_index, hilbert_indices_prequantized, hilbert_quantize, hilbert_sort_by_stable, hilbert_sort_by_unstable, hilbert_sorted_indices, }; -use delaunay::prelude::query::ConvexHull; +use delaunay::prelude::query::{ConvexHull, QueryError}; use delaunay::prelude::repair::{ DelaunayCheckPolicy, DelaunayRepairDiagnostics, DelaunayRepairError, DelaunayRepairOperation, DelaunayRepairOutcome, DelaunayRepairStats, DelaunayRepairVerificationContext, @@ -64,8 +64,9 @@ use delaunay::prelude::tds::Tds; use delaunay::prelude::tds::{InvariantErrorSummaryDetail, NeighborSlot, TdsErrorKind}; use delaunay::prelude::triangulation::{ FacetIssuesMap as TriangulationFacetIssuesMap, FastKernel as TriangulationFastKernel, - InsertionError as TriangulationInsertionError, TdsError as TriangulationTdsError, - TopologyGuarantee as TriangulationTopologyGuarantee, Triangulation as GenericTriangulation, + InsertionError as TriangulationInsertionError, QueryError as TriangulationQueryError, + TdsError as TriangulationTdsError, TopologyGuarantee as TriangulationTopologyGuarantee, + Triangulation as GenericTriangulation, TriangulationConstructionError as GenericTriangulationConstructionError, ValidationPolicy as TriangulationValidationPolicy, vertex as triangulation_vertex, }; @@ -98,6 +99,8 @@ enum PreludeExportTestError { Delaunayize(#[from] DelaunayizeError), #[error(transparent)] Insertion(#[from] InsertionError), + #[error(transparent)] + Query(#[from] QueryError), } /// Proves the focused flips prelude exports the trait bound expected by benchmarks. @@ -182,7 +185,7 @@ fn preludes_cover_bench_apis() -> Result<(), PreludeExportTestError> { let dt = DelaunayTriangulation::new_with_options(&vertices, options)?; assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); - assert!(dt.boundary_facets().count() > 0); + assert!(dt.boundary_facets()?.count() > 0); assert!(ConvexHull::from_triangulation(dt.as_triangulation()).is_ok()); assert!(dt.validate().is_ok()); assert_bistellar_flips(&dt); @@ -337,11 +340,13 @@ fn triangulation_prelude_covers_generic_layer() -> Result<(), GenericTriangulati let empty_issues = TriangulationFacetIssuesMap::default(); let removed = tri - .repair_local_facet_issues(&empty_issues) + .repair_local_facet_issues(&empty_issues, 0) .expect("empty issue set should not fail generic local repair"); assert_eq!(removed, 0); + assert_eq!(tri.boundary_facets().unwrap().count(), 0); assert_send_sync_unpin::(); + assert_send_sync_unpin::(); assert_send_sync_unpin::(); Ok(()) } diff --git a/tests/proptest_triangulation.rs b/tests/proptest_triangulation.rs index 9f82249d..3a98c1e3 100644 --- a/tests/proptest_triangulation.rs +++ b/tests/proptest_triangulation.rs @@ -730,7 +730,7 @@ macro_rules! test_facet_topology_invariant { // If there are any issues, repair them if let Some(issues) = tri.detect_local_facet_issues(&simplex_keys)? { - let _removed = tri.repair_local_facet_issues(&issues)?; + let _removed = tri.repair_local_facet_issues(&issues, usize::MAX)?; // After repair, re-check - should have no issues let simplex_keys_after: Vec<_> = tri.simplices().map(|(k, _)| k).collect(); diff --git a/tests/trait_bound_ergonomics.rs b/tests/trait_bound_ergonomics.rs index 4c06217e..4a64420c 100644 --- a/tests/trait_bound_ergonomics.rs +++ b/tests/trait_bound_ergonomics.rs @@ -15,7 +15,7 @@ fn read_only_topology_apis_accept_non_datatype_payloads() { assert_eq!(tri.number_of_vertices(), 0); assert_eq!(tri.number_of_simplices(), 0); - assert_eq!(tri.boundary_facets().count(), 0); + assert_eq!(tri.boundary_facets().unwrap().count(), 0); let index = tri.build_adjacency_index().unwrap(); assert!(index.vertex_to_simplices.is_empty()); diff --git a/tests/triangulation_builder.rs b/tests/triangulation_builder.rs index 4528fabf..5539bbf6 100644 --- a/tests/triangulation_builder.rs +++ b/tests/triangulation_builder.rs @@ -15,6 +15,7 @@ use delaunay::prelude::construction::{ ExplicitTdsErrorKind, InsertionOrderStrategy, TopologyGuarantee, Vertex, VertexBuilder, vertex, }; use delaunay::prelude::geometry::{Coordinate, Point, RobustKernel}; +use delaunay::prelude::insertion::InsertionErrorSourceKind; use delaunay::prelude::repair::DelaunayRepairError; use delaunay::prelude::tds::{InvariantErrorSummaryDetail, TriangulationValidationErrorKind}; use delaunay::prelude::topology::spaces::{GlobalTopology, TopologyKind, ToroidalConstructionMode}; @@ -568,7 +569,23 @@ fn test_explicit_toroidal_torus_euler_mismatch_without_override() { ), ); } - other => panic!("expected explicit topology validation failure, got {other:?}"), + DelaunayTriangulationConstructionError::ExplicitConstruction( + ExplicitConstructionError::OrientationNormalization { source }, + ) => { + assert_eq!( + source.kind, + ExplicitInsertionErrorKind::TopologyValidationFailed + ); + assert_eq!( + source.source_kind, + Some(InsertionErrorSourceKind::Triangulation( + TriangulationValidationErrorKind::OrientationPromotionNonConvergence, + )), + ); + } + other => { + panic!("expected explicit topology or orientation-normalization failure, got {other:?}") + } } } @@ -718,7 +735,7 @@ fn test_explicit_3d_two_tetrahedra() { vertex!([1.0, 0.0, 0.0]), vertex!([0.0, 1.0, 0.0]), vertex!([0.0, 0.0, 1.0]), - vertex!([1.0, 1.0, 1.0]), + vertex!([1.0, 1.0, -1.0]), ]; // Two tetrahedra sharing face (0, 1, 2) let simplices = vec![vec![0, 1, 2, 3], vec![0, 1, 2, 4]]; From d8c8666be042df016eed5e5905bdd30ba40420a4 Mon Sep 17 00:00:00 2001 From: Adam Getchell Date: Tue, 19 May 2026 11:56:54 -0700 Subject: [PATCH 3/3] fix(core): preserve local repair budget failures - Preserve local repair budget exhaustion as a structured initial-simplex error so callers can inspect the attempted and maximum removal counts. - Validate TDS and triangulation invariants after vertex removal so invalid repair states trigger transactional rollback. - Clarify repair documentation around cavity fragility, references, and flattened public import paths. --- src/core/algorithms/flips.rs | 2 +- src/core/algorithms/incremental_insertion.rs | 73 ++++++++++++++-- src/core/repair.rs | 92 ++++++++++++++++---- src/core/tds.rs | 35 +++++++- src/delaunay/insertion.rs | 48 ++++++++-- 5 files changed, 213 insertions(+), 37 deletions(-) diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index 88d91686..e5a263f7 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -9491,6 +9491,7 @@ fn enqueue_ridge( #[cfg(test)] mod tests { use super::*; + use crate::DelaunayTriangulation; use crate::core::algorithms::incremental_insertion::{ DelaunayRepairFailureContext, repair_neighbor_pointers, }; @@ -9500,7 +9501,6 @@ mod tests { use crate::geometry::kernel::{AdaptiveKernel, FastKernel}; use crate::repair::DelaunayRepairOperation; use crate::topology::traits::topological_space::ToroidalConstructionMode; - use crate::triangulation::DelaunayTriangulation; use crate::vertex; use approx::assert_relative_eq; use proptest::prelude::*; diff --git a/src/core/algorithms/incremental_insertion.rs b/src/core/algorithms/incremental_insertion.rs index abd2311e..ee6959ab 100644 --- a/src/core/algorithms/incremental_insertion.rs +++ b/src/core/algorithms/incremental_insertion.rs @@ -519,6 +519,17 @@ pub enum InitialSimplexConstructionError { coordinates: String, }, + /// Local repair would remove more simplices than the active budget allowed. + #[error( + "local repair removal budget exceeded while building initial simplex: attempted {attempted}, max {max_simplices_removed}" + )] + LocalRepairBudgetExceeded { + /// Maximum number of simplices this repair stage was allowed to remove. + max_simplices_removed: usize, + /// Number of simplices the repair stage attempted to remove. + attempted: usize, + }, + /// An insertion-stage-only construction error escaped initial-simplex construction. #[error( "unexpected insertion-stage construction error while building initial simplex: {message}" @@ -602,11 +613,9 @@ impl From for InitialSimplexConstructionError { TriangulationConstructionError::LocalRepairBudgetExceeded { max_simplices_removed, attempted, - } => Self::UnexpectedInsertionStage { - message: format!( - "local repair budget exceeded during initial simplex construction: \ - attempted {attempted}, max {max_simplices_removed}" - ), + } => Self::LocalRepairBudgetExceeded { + max_simplices_removed, + attempted, }, TriangulationConstructionError::FinalTopologyValidation { source, .. } => { Self::UnexpectedInsertionStage { @@ -1636,6 +1645,7 @@ impl InsertionError { | InitialSimplexConstructionError::InsufficientVertices { .. } | InitialSimplexConstructionError::InternalInconsistency { .. } | InitialSimplexConstructionError::DuplicateCoordinates { .. } + | InitialSimplexConstructionError::LocalRepairBudgetExceeded { .. } | InitialSimplexConstructionError::UnexpectedInsertionStage { .. } => false, }, CavityFillingError::NeighborRebuild { reason } => match reason { @@ -4214,6 +4224,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::DelaunayTriangulation; use crate::core::algorithms::flips::{ DelaunayRepairDiagnostics, DelaunayRepairVerificationContext, FlipError, RepairQueueOrder, }; @@ -4224,7 +4235,6 @@ mod tests { use crate::geometry::kernel::FastKernel; use crate::geometry::traits::coordinate::{Coordinate, CoordinateConversionError}; use crate::topology::characteristics::euler::TopologyClassification; - use crate::triangulation::DelaunayTriangulation; use crate::vertex; use slotmap::KeyData; @@ -4967,6 +4977,24 @@ mod tests { } } + #[test] + fn test_insertion_error_summary_preserves_repair_budget_error() { + let source = InsertionError::MaxSimplicesRemovedExceeded { + max_simplices_removed: 2, + attempted: 3, + }; + let summary = InsertionErrorSummary::from(source.clone()); + + assert_eq!( + summary.kind, + InsertionErrorKind::MaxSimplicesRemovedExceeded + ); + assert_eq!(summary.source_kind, None); + assert_eq!(summary.message, source.to_string()); + assert!(!summary.retryable); + assert!(!source.is_retryable()); + } + #[test] fn test_insertion_error_summary_retryability_covers_tds_source_kinds() { let geometric = InsertionErrorSummary { @@ -5013,6 +5041,10 @@ mod tests { } #[test] + #[expect( + clippy::too_many_lines, + reason = "Covers retryability for each structured cavity-filling payload" + )] fn test_cavity_filling_retryability_inspects_construction_payloads() { let geometry_failure = TdsValidationFailure::Geometric { source: GeometricError::DegenerateOrientation { @@ -5084,6 +5116,17 @@ mod tests { } .is_retryable() ); + assert!( + !InsertionError::CavityFilling { + reason: CavityFillingError::InitialSimplexConstruction { + reason: InitialSimplexConstructionError::LocalRepairBudgetExceeded { + max_simplices_removed: 2, + attempted: 3, + }, + }, + } + .is_retryable() + ); assert!( InsertionError::CavityFilling { reason: CavityFillingError::NeighborRebuild { @@ -5111,6 +5154,24 @@ mod tests { ); } + #[test] + fn test_initial_simplex_construction_error_preserves_repair_budget() { + let source = TriangulationConstructionError::LocalRepairBudgetExceeded { + max_simplices_removed: 2, + attempted: 3, + }; + + let converted = InitialSimplexConstructionError::from(source); + + assert_eq!( + converted, + InitialSimplexConstructionError::LocalRepairBudgetExceeded { + max_simplices_removed: 2, + attempted: 3, + } + ); + } + // InsertionError::is_retryable() tests #[test] diff --git a/src/core/repair.rs b/src/core/repair.rs index 35e2e1ff..be191d69 100644 --- a/src/core/repair.rs +++ b/src/core/repair.rs @@ -262,8 +262,16 @@ where /// 6. Removing the vertex itself /// /// **Fan Triangulation**: The cavity is filled by picking one boundary vertex as an apex - /// and connecting it to all boundary facets. This is fast and maintains all topological - /// invariants, though it may create poorly-shaped simplices in some cases. + /// and connecting it to all boundary facets. This follows the local cavity-retriangulation + /// lineage used by Bowyer-Watson insertion and the computational-geometry treatment in + /// Edelsbrunner and Preparata-Shamos; see `REFERENCES.md` entries \[1\]-\[5\] for + /// `remove_vertex` source context and the robust predicate background from Shewchuk. + /// + /// The `remove_vertex` fan step is numerically and topologically fragile when the cavity is + /// degenerate or nearly coplanar, when epsilon thresholds are too small for the active scalar + /// range, or when candidate simplices are inverted. Mitigate those cases with robust + /// predicates, explicit epsilon thresholds, bounded repair budgets, and transactional + /// validation fallbacks. /// /// # Arguments /// @@ -298,11 +306,9 @@ where .collect(); if simplices_to_remove.is_empty() { - // Vertex exists but has no incident simplices - use Tds removal - return self - .tds - .remove_vertex(vertex_key) - .map_err(|e| InvariantError::Tds(e.into_inner())); + // Vertex exists but has no incident simplices; remove it only if the + // resulting triangulation satisfies the same invariant checks. + return self.remove_vertex_with_invariant_checks(vertex_key); } // Extract cavity boundary BEFORE removing simplices @@ -315,11 +321,9 @@ where // If boundary is empty, we're removing the entire triangulation if boundary_facets.is_empty() { - // Use Tds removal for empty boundary case - return self - .tds - .remove_vertex(vertex_key) - .map_err(|e| InvariantError::Tds(e.into_inner())); + // Use TDS removal for the empty-boundary case, then validate so + // lower-dimensional remnants are rejected and rolled back. + return self.remove_vertex_with_invariant_checks(vertex_key); } // Pick apex vertex for fan triangulation (first vertex of first boundary facet) @@ -399,6 +403,9 @@ where .remove_vertex(vertex_key) .map_err(|e| InvariantError::Tds(e.into_inner()))?; + self.tds.is_valid().map_err(InvariantError::Tds)?; + self.is_valid()?; + Ok(simplices_removed) })(); @@ -411,6 +418,36 @@ where } } + /// Removes a vertex via direct TDS mutation and rolls back unless all triangulation + /// invariants still hold. + /// + /// This handles fallback paths that do not retriangulate a cavity, such as isolated vertices + /// or empty-boundary removals. Those paths can otherwise leave lower-dimensional remnants that + /// are structurally valid at the TDS layer but invalid as a triangulation. + fn remove_vertex_with_invariant_checks( + &mut self, + vertex_key: VertexKey, + ) -> Result { + let tds_snapshot = self.tds.clone_for_rollback(); + let result = (|| -> Result { + let simplices_removed = self + .tds + .remove_vertex(vertex_key) + .map_err(|e| InvariantError::Tds(e.into_inner()))?; + self.tds.is_valid().map_err(InvariantError::Tds)?; + self.is_valid()?; + Ok(simplices_removed) + })(); + + match result { + Ok(simplices_removed) => Ok(simplices_removed), + Err(error) => { + self.tds = tds_snapshot; + Err(error) + } + } + } + /// Pick an apex vertex for fan triangulation. /// /// Selects the first vertex from the first boundary facet as the apex. @@ -805,13 +842,22 @@ where /// would remove more simplices than `max_simplices_removed` allows; in that /// case the original TDS is restored before returning the error. /// + /// `repair_local_facet_issues` uses a localized radius-ratio heuristic to choose + /// problematic simplices for removal and repair. The heuristic is inspired by the same + /// local cavity and simplex-quality ideas cited for `remove_vertex`; see `REFERENCES.md` + /// entries \[1\]-\[5\]. It may fail or choose an overly aggressive repair near degenerate or + /// nearly-coplanar cavities, inverted simplices, or scalar ranges where small numeric epsilons + /// hide facet distinctions. Use robust predicates, explicit epsilon thresholds, bounded + /// budgets, and transactional fallbacks when calling it from public repair paths. + /// /// # Examples /// /// ```rust /// use delaunay::prelude::construction::{ /// DelaunayTriangulation, DelaunayTriangulationConstructionError, /// }; - /// use delaunay::prelude::triangulation::{FacetIssuesMap, InsertionError, vertex}; + /// use delaunay::prelude::insertion::InsertionError; + /// use delaunay::prelude::triangulation::{FacetIssuesMap, vertex}; /// /// # #[derive(Debug, thiserror::Error)] /// # enum ExampleError { @@ -888,12 +934,12 @@ where #[cfg(test)] mod tests { use super::*; + use crate::DelaunayTriangulation; use crate::core::collections::{CavityBoundaryBuffer, NeighborBuffer}; use crate::core::simplex::NeighborSlot; use crate::core::tds::Tds; use crate::core::vertex::Vertex; use crate::geometry::kernel::FastKernel; - use crate::triangulation::DelaunayTriangulation; use crate::vertex; use slotmap::KeyData; @@ -1437,10 +1483,22 @@ mod tests { let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices).unwrap(); + let initial_vertices = dt.number_of_vertices(); + let initial_simplices = dt.number_of_simplices(); let vertex_key = dt.vertices().next().unwrap().0; - let removed = dt.remove_vertex(vertex_key).unwrap(); - assert!(removed >= 1); - assert_eq!(dt.number_of_vertices(), 2); + + let error = dt.remove_vertex(vertex_key).unwrap_err(); + + assert!( + matches!( + error, + InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { .. }) + ), + "expected isolated-vertex invariant failure, got {error:?}" + ); + assert_eq!(dt.number_of_vertices(), initial_vertices); + assert_eq!(dt.number_of_simplices(), initial_simplices); + assert!(dt.tds().contains_vertex_key(vertex_key)); } // ========================================================================= diff --git a/src/core/tds.rs b/src/core/tds.rs index 66f6d9a2..757aecdb 100644 --- a/src/core/tds.rs +++ b/src/core/tds.rs @@ -7188,6 +7188,13 @@ mod tests { TriangulationValidationError::Disconnected { simplex_count: 2 }, TriangulationValidationErrorKind::Disconnected, ), + ( + TriangulationValidationError::OrientationPromotionNonConvergence { + residual_count: 1, + sampled: vec![SimplexKey::from(KeyData::from_ffi(4))], + }, + TriangulationValidationErrorKind::OrientationPromotionNonConvergence, + ), ]; for (source, expected) in cases { @@ -7731,11 +7738,21 @@ mod tests { vertex!([1.0, 0.0, 0.0]), vertex!([0.0, 1.0, 0.0]), vertex!([0.0, 0.0, 1.0]), - vertex!([1.0, 1.0, 1.0]), + vertex!([0.2, 0.2, 0.2]), ]; let mut dt_3d: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices_3d).unwrap(); - let vertex_key = dt_3d.vertices().next().unwrap().0; + let vertex_key = dt_3d + .vertices() + .find(|(_, vertex)| { + let coords = vertex.point().coords(); + coords + .iter() + .zip([0.2, 0.2, 0.2]) + .all(|(coord, expected)| (*coord - expected).abs() < 1e-12) + }) + .unwrap() + .0; let simplices_removed = dt_3d.remove_vertex(vertex_key).unwrap(); assert!(simplices_removed > 0); assert!(dt_3d.as_triangulation().tds.is_valid().is_ok()); @@ -7749,11 +7766,21 @@ mod tests { vertex!([0.0, 1.0, 0.0, 0.0]), vertex!([0.0, 0.0, 1.0, 0.0]), vertex!([0.0, 0.0, 0.0, 1.0]), - vertex!([1.0, 1.0, 1.0, 1.0]), + vertex!([0.2, 0.2, 0.2, 0.2]), ]; let mut dt_4d: DelaunayTriangulation<_, (), (), 4> = DelaunayTriangulation::new(&vertices_4d).unwrap(); - let vertex_key = dt_4d.vertices().next().unwrap().0; + let vertex_key = dt_4d + .vertices() + .find(|(_, vertex)| { + let coords = vertex.point().coords(); + coords + .iter() + .zip([0.2, 0.2, 0.2, 0.2]) + .all(|(coord, expected)| (*coord - expected).abs() < 1e-12) + }) + .unwrap() + .0; let simplices_removed = dt_4d.remove_vertex(vertex_key).unwrap(); assert!(simplices_removed > 0); assert!(dt_4d.as_triangulation().tds.is_valid().is_ok()); diff --git a/src/delaunay/insertion.rs b/src/delaunay/insertion.rs index 453b727a..92650152 100644 --- a/src/delaunay/insertion.rs +++ b/src/delaunay/insertion.rs @@ -560,7 +560,7 @@ where /// Removes a vertex and retriangulates the resulting cavity using fan triangulation. /// - /// This operation delegates to `Triangulation::remove_vertex()` which: + /// This operation delegates to the core triangulation layer, which: /// 1. Finds all simplices containing the vertex /// 2. Removes those simplices (creating a cavity) /// 3. Fills the cavity with fan triangulation @@ -571,10 +571,13 @@ where /// consistent adjacency), this method collapses it via the **inverse k=1** bistellar /// flip. Otherwise it falls back to fan triangulation. /// - /// The triangulation remains topologically valid after removal. However, both the - /// inverse k=1 fast-path and fan triangulation may temporarily violate the Delaunay - /// property in some cases. If the [`DelaunayRepairPolicy`] allows it, a flip-based - /// repair pass is run automatically after removal. + /// This operation is topology-preserving on success: it returns `Ok` only after the + /// post-removal triangulation satisfies the required manifold and topology invariants. A + /// candidate removal that would collapse the mesh to a lower-dimensional remnant or isolate + /// remaining vertices is rejected as an [`InvariantError::Triangulation`] failure, and the + /// pre-removal state is restored. Both the inverse k=1 fast-path and fan triangulation may + /// temporarily violate the Delaunay property in some cases. If the [`DelaunayRepairPolicy`] + /// allows it, a flip-based repair pass is run automatically after removal. /// /// The post-removal repair and orientation canonicalization steps are /// transactional: if either step fails, this method restores the triangulation @@ -603,11 +606,15 @@ where /// # Errors /// /// Returns [`InvariantError`] if: - /// - The inverse k=1 flip encounters a neighbor-wiring failure (`InvariantError::Tds`). - /// - Fan retriangulation fails (`InvariantError::Tds`). + /// - The inverse k=1 flip encounters a neighbor-wiring failure ([`InvariantError::Tds`]). + /// - Fan retriangulation fails ([`InvariantError::Tds`]). + /// - Post-removal topology validation fails, for example because removal would leave + /// isolated vertices or a lower-dimensional remnant + /// ([`InvariantError::Triangulation`]). /// - Delaunay flip-based repair fails after removal - /// (`InvariantError::Delaunay(DelaunayTriangulationValidationError::RepairOperationFailed { .. })`). - /// - Orientation canonicalization fails after repair (`InvariantError::Tds`). + /// ([`InvariantError::Delaunay`] wrapping + /// [`DelaunayTriangulationValidationError::RepairOperationFailed`]). + /// - Orientation canonicalization fails after repair ([`InvariantError::Tds`]). /// /// # Examples /// @@ -638,6 +645,29 @@ where /// // Vertex removal preserves topology; automatic repair is attempted when enabled. /// assert!(dt.as_triangulation().validate().is_ok()); /// ``` + /// + /// Removals that would leave a non-manifold remnant fail and roll back: + /// + /// ```rust + /// use delaunay::prelude::*; + /// + /// let vertices = [ + /// vertex!([0.0, 0.0]), + /// vertex!([1.0, 0.0]), + /// vertex!([0.0, 1.0]), + /// ]; + /// let mut dt: DelaunayTriangulation<_, (), (), 2> = + /// DelaunayTriangulation::new(&vertices).unwrap(); + /// let vertex_key = dt.vertices().next().unwrap().0; + /// + /// let err = dt.remove_vertex(vertex_key).unwrap_err(); + /// assert!(matches!( + /// err, + /// InvariantError::Triangulation(TriangulationValidationError::IsolatedVertex { .. }) + /// )); + /// assert_eq!(dt.number_of_vertices(), 3); + /// assert_eq!(dt.number_of_simplices(), 1); + /// ``` pub fn remove_vertex(&mut self, vertex_key: VertexKey) -> Result { let Some(removed_vertex) = self.tri.tds.vertex(vertex_key) else { return Ok(0);