diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index b445895e..8539fb82 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.26" + UV_VERSION: "0.11.27" 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 9e88a59e..f109cd2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ env: RUMDL_VERSION: "0.2.28" TAPLO_VERSION: "0.10.0" TYPOS_VERSION: "1.48.0" - UV_VERSION: "0.11.26" + UV_VERSION: "0.11.27" ZIZMOR_VERSION: "1.26.1" jobs: diff --git a/.github/workflows/generate-baseline.yml b/.github/workflows/generate-baseline.yml index ddb69b18..d564eefb 100644 --- a/.github/workflows/generate-baseline.yml +++ b/.github/workflows/generate-baseline.yml @@ -26,7 +26,7 @@ permissions: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 - UV_VERSION: "0.11.26" + UV_VERSION: "0.11.27" # 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/papers.yml b/.github/workflows/papers.yml index 370a70ba..c481e728 100644 --- a/.github/workflows/papers.yml +++ b/.github/workflows/papers.yml @@ -52,11 +52,11 @@ env: JUST_VERSION: "1.55.1" TECTONIC_VERSION: "0.16.9" TEX_FMT_VERSION: "0.5.7" - UV_VERSION: "0.11.26" + UV_VERSION: "0.11.27" jobs: papers: - runs-on: ubuntu-latest + runs-on: macos-latest timeout-minutes: 30 steps: @@ -86,17 +86,8 @@ jobs: - name: Install paper system dependencies run: | - sudo apt-get update - sudo apt-get install --yes \ - chktex \ - libfontconfig1-dev \ - libfreetype6-dev \ - libgraphite2-dev \ - libharfbuzz-dev \ - libicu-dev \ - libpng-dev \ - pkg-config \ - zlib1g-dev + brew update + brew install chktex libpng pkgconf - name: Install just uses: taiki-e/cache-cargo-install-action@417450f3c33ee20393705369577571770643d4c7 # v3.0.7 diff --git a/.github/workflows/release-benchmarks.yml b/.github/workflows/release-benchmarks.yml index b20f9772..3a280cd2 100644 --- a/.github/workflows/release-benchmarks.yml +++ b/.github/workflows/release-benchmarks.yml @@ -19,7 +19,7 @@ concurrency: env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 - UV_VERSION: "0.11.26" + UV_VERSION: "0.11.27" jobs: release-baseline: diff --git a/.github/workflows/semgrep-sarif.yml b/.github/workflows/semgrep-sarif.yml index 6d452520..724e47aa 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.26" + UV_VERSION: "0.11.27" jobs: semgrep-sarif: diff --git a/README.md b/README.md index abda25e9..150412bd 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,7 @@ This project is licensed under the [BSD 3-Clause License](https://github.com/acg [Pachner moves]: https://en.wikipedia.org/wiki/Pachner_move [PL-manifold]: https://en.wikipedia.org/wiki/Piecewise_linear_manifold [Pseudomanifold]: https://en.wikipedia.org/wiki/Pseudomanifold -[readme-hero]: docs/images/delaunay_3d_readme.png +[readme-hero]: https://raw.githubusercontent.com/acgetchell/delaunay/main/docs/images/delaunay_3d_readme.png [Secondary maps]: docs/workflows.md#builder-api-auxiliary-vertex-and-simplex-data [Simulation of Simplicity]: docs/numerical_robustness_guide.md#simulation-of-simplicity-sos [Validation Guide]: docs/validation.md diff --git a/benches/ci_performance_suite.rs b/benches/ci_performance_suite.rs index f61ee1d2..235909b9 100644 --- a/benches/ci_performance_suite.rs +++ b/benches/ci_performance_suite.rs @@ -44,8 +44,9 @@ use criterion::{ BatchSize, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, }; use delaunay::flips::{FacetHandle, RidgeHandle, SimplexKey}; +use delaunay::prelude::collections::FastHashMap; use delaunay::prelude::construction::{ - ConstructionOptions, DelaunayTriangulation, RetryPolicy, Vertex, + ConstructionOptions, DelaunayTriangulation, DelaunayTriangulationBuilder, RetryPolicy, Vertex, }; use delaunay::prelude::generators::generate_random_points_in_range_seeded; use delaunay::prelude::geometry::{AdaptiveKernel, CoordinateRange, Point}; @@ -89,9 +90,18 @@ const CANARY_COUNT_5D: usize = 25; const INSERT_BATCH_COUNT_2D_3D: usize = 10; const INSERT_BATCH_COUNT_4D: usize = 6; const INSERT_BATCH_COUNT_5D: usize = 4; +const EXPLICIT_IMPORT_COUNT_2D: usize = 120; +const EXPLICIT_IMPORT_COUNT_3D: usize = 30; +const EXPLICIT_IMPORT_COUNT_4D: usize = 16; +const EXPLICIT_IMPORT_COUNT_5D: usize = 10; type SeedSearchResult = Option<(u64, Vec>, Vec>)>; type BenchTriangulation = DelaunayTriangulation, (), (), D>; +struct ExplicitImportFixture { + vertices: Vec>, + simplices: Vec>, +} + fn finite_point(coords: [f64; D]) -> Point { Point::try_new(coords).unwrap_or_else(|_| std::process::abort()) } @@ -186,6 +196,16 @@ fn insert_benchmark_ids() -> String { .join(";") } +fn explicit_import_benchmark_ids() -> String { + [ + format!("explicit_import/import_2d/{EXPLICIT_IMPORT_COUNT_2D}"), + format!("explicit_import/import_3d/{EXPLICIT_IMPORT_COUNT_3D}"), + format!("explicit_import/import_4d/{EXPLICIT_IMPORT_COUNT_4D}"), + format!("explicit_import/import_5d/{EXPLICIT_IMPORT_COUNT_5D}"), + ] + .join(";") +} + fn api_benchmark_entries() -> Vec { vec![ ApiBenchmarkEntry { @@ -230,6 +250,13 @@ fn api_benchmark_entries() -> Vec { benchmark_ids: insert_benchmark_ids(), note: "insert_batches_into_calibrated_well_conditioned_and_adversarial_triangulations", }, + ApiBenchmarkEntry { + group: "explicit_import", + public_api: "DelaunayTriangulationBuilder::try_from_vertices_and_simplices(...).construction_options(without_final_delaunay_enforcement).build", + dimensions: "2,3,4,5", + benchmark_ids: explicit_import_benchmark_ids(), + note: "reimport_valid_levels_1_through_4_connectivity_from_public_vertex_and_simplex_iterators", + }, ApiBenchmarkEntry { group: "bistellar_flips", public_api: "BistellarFlips::{flip_k1_insert,flip_k1_remove,flip_k2,flip_k2_inverse_from_edge,flip_k3,flip_k3_inverse_from_triangle}", @@ -397,6 +424,56 @@ fn prepare_adv_dt(dim_seed: u64, count: usize) -> BenchTriangula .or_abort() } +/// Export a valid Delaunay triangulation through public iterators for explicit reimport. +fn prepare_explicit_import_fixture( + dim_seed: u64, + count: usize, +) -> ExplicitImportFixture { + let seed = dim_seed.wrapping_add(count as u64); + let points = generate_random_points_in_range_seeded::( + count, + CoordinateRange::try_new(-100.0_f64, 100.0).or_abort(), + seed, + ) + .or_abort(); + let source_vertices = try_vertices_from_points(&points).or_abort(); + let attempts = retry_attempts(8); + let options = ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { + attempts, + base_seed: Some(seed), + }); + let dt: BenchTriangulation = DelaunayTriangulation::builder(&source_vertices) + .construction_options(options) + .build() + .or_abort(); + let mut key_to_index = FastHashMap::default(); + let mut vertices = Vec::with_capacity(dt.number_of_vertices()); + + for (index, (vertex_key, vertex)) in dt.vertices().enumerate() { + key_to_index.insert(vertex_key, index); + vertices.push(*vertex); + } + + let mut simplices = Vec::with_capacity(dt.number_of_simplices()); + for (_, simplex) in dt.simplices() { + let mut spec = Vec::with_capacity(D + 1); + for vertex_key in simplex.vertices() { + let Some(&vertex_index) = key_to_index.get(vertex_key) else { + abort_benchmark(format_args!( + "{D}D explicit import fixture simplex references an unknown vertex key" + )); + }; + spec.push(vertex_index); + } + simplices.push(spec); + } + + ExplicitImportFixture { + vertices, + simplices, + } +} + fn prepare_inserts( dim_seed: u64, count: usize, @@ -1262,6 +1339,38 @@ fn bench_insert_case( ); } +fn bench_explicit_import_case( + group: &mut BenchmarkGroup<'_, WallTime>, + fixture: &ExplicitImportFixture, +) { + group.throughput(Throughput::Elements(fixture.simplices.len() as u64)); + group.bench_function( + BenchmarkId::new( + format!("import_{D}d"), + format!( + "vertices_{}_simplices_{}", + fixture.vertices.len(), + fixture.simplices.len() + ), + ), + |b| { + b.iter(|| { + let dt = DelaunayTriangulationBuilder::try_from_vertices_and_simplices( + &fixture.vertices, + &fixture.simplices, + ) + .or_abort() + .construction_options( + ConstructionOptions::default().without_final_delaunay_enforcement(), + ) + .build() + .or_abort(); + black_box(dt); + }); + }, + ); +} + fn benchmark_boundary_facets(c: &mut Criterion) { print_manifest_once(); if discover_seeds_enabled() { @@ -1609,6 +1718,38 @@ fn benchmark_insert(c: &mut Criterion) { group.finish(); } +fn benchmark_explicit_import(c: &mut Criterion) { + print_manifest_once(); + if discover_seeds_enabled() { + return; + } + let filters = criterion_filters(); + let mut group = c.benchmark_group("explicit_import"); + group.sample_size(10); + + if benchmark_selected(&filters, "explicit_import/import_2d") { + let fixture_2d = prepare_explicit_import_fixture::<2>(42, EXPLICIT_IMPORT_COUNT_2D); + bench_explicit_import_case(&mut group, &fixture_2d); + } + + if benchmark_selected(&filters, "explicit_import/import_3d") { + let fixture_3d = prepare_explicit_import_fixture::<3>(123, EXPLICIT_IMPORT_COUNT_3D); + bench_explicit_import_case(&mut group, &fixture_3d); + } + + if benchmark_selected(&filters, "explicit_import/import_4d") { + let fixture_4d = prepare_explicit_import_fixture::<4>(456, EXPLICIT_IMPORT_COUNT_4D); + bench_explicit_import_case(&mut group, &fixture_4d); + } + + if benchmark_selected(&filters, "explicit_import/import_5d") { + let fixture_5d = prepare_explicit_import_fixture::<5>(789, EXPLICIT_IMPORT_COUNT_5D); + bench_explicit_import_case(&mut group, &fixture_5d); + } + + group.finish(); +} + /// Registers the complete 2D-5D public bistellar flip benchmark matrix. fn benchmark_bistellar_flips(c: &mut Criterion) { print_manifest_once(); @@ -1778,6 +1919,7 @@ criterion_group!( benchmark_convex_hull_queries, benchmark_validation, benchmark_insert, + benchmark_explicit_import, benchmark_bistellar_flips ); criterion_main!(benches); diff --git a/docs/api_design.md b/docs/api_design.md index 91b446ee..c109a2a1 100644 --- a/docs/api_design.md +++ b/docs/api_design.md @@ -600,7 +600,8 @@ fn main() -> Result<(), ExampleError> { 2. **Delaunay flip repair** — k=2/k=3 bistellar flips to restore the empty-circumsphere property. 3. **Optional fallback rebuild** — rebuilds from the vertex set when both - repair passes fail (`DelaunayizeConfig { fallback_rebuild: true, .. }`). + repair passes fail + (`DelaunayizeConfig::default().with_fallback_rebuild(true)`). If a failed topology repair is recovered by fallback rebuild, `outcome.topology_repair.succeeded` remains `false`; use `outcome.used_fallback_rebuild` to distinguish successful rebuild recovery diff --git a/docs/dev/tooling-alignment.md b/docs/dev/tooling-alignment.md index 109dcde9..81da599b 100644 --- a/docs/dev/tooling-alignment.md +++ b/docs/dev/tooling-alignment.md @@ -228,7 +228,7 @@ The useful updates ported in this pass are: separately installs `chktex` from the system package manager because it is TeX distribution tooling rather than a Rust CLI, and installs the Linux native bridge-library headers required when compiling Tectonic from Cargo. All - uv-backed workflows use uv 0.11.26 to match the local Python tooling + uv-backed workflows use uv 0.11.27 to match the local Python tooling bootstrap. - `.codecov.yml` now ratchets Delaunay's coverage policy above the older causal-triangulations baseline without copying la-stack's near-total diff --git a/examples/delaunayize_repair.rs b/examples/delaunayize_repair.rs index dc9e06fe..b6b27e20 100644 --- a/examples/delaunayize_repair.rs +++ b/examples/delaunayize_repair.rs @@ -240,12 +240,10 @@ fn custom_config_2d() -> Result<(), DelaunayizeRepairExampleError> { let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulationBuilder::new(&vertices).build()?; - let config = DelaunayizeConfig { - topology_max_iterations: 10, - topology_max_simplices_removed: 100, - fallback_rebuild: true, - delaunay_max_flips: None, - }; + let config = DelaunayizeConfig::default() + .with_topology_max_iterations(10) + .with_topology_max_simplices_removed(100) + .with_fallback_rebuild(true); println!( " Config: max_iterations={}, max_simplices_removed={}, fallback={}", diff --git a/src/config.rs b/src/config.rs index a8c04bbf..e0178a1d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -243,7 +243,7 @@ enum GenerateCommand { #[derive(Debug)] struct GenerateConfig { kind: GenerateKind, - vertices: usize, + vertices: NonZeroUsize, distribution: GenerateDistribution, seed: u64, output: Option, @@ -252,17 +252,17 @@ struct GenerateConfig { impl GenerateConfig { /// Validate dimension-dependent generation limits. fn try_new(args: GenerateArgs) -> Result { - if args.vertices < D + 1 { - return Err(CliError::TooFewVertices { + let vertices = NonZeroUsize::new(args.vertices) + .filter(|vertices| vertices.get() > D) + .ok_or(CliError::TooFewVertices { dimension: D, vertices: args.vertices, minimum: D + 1, - }); - } + })?; Ok(Self { kind: args.kind, - vertices: args.vertices, + vertices, distribution: args.distribution, seed: args.seed, output: args.output, @@ -468,18 +468,18 @@ fn run_validation_demo(config: &ValidationDemoConfig) -> Result<(), CliError> { /// Build a random PL-manifold Delaunay triangulation for CLI export. fn build_generated_delaunay( - vertex_count: usize, + vertex_count: NonZeroUsize, seed: u64, distribution: GenerateDistribution, ) -> Result, (), (), D>, CliError> { let points = match distribution { GenerateDistribution::Cube => generate_random_points_in_range_seeded::( - vertex_count, + vertex_count.get(), CoordinateRange::try_new(0.0_f64, 1.0)?, seed, )?, GenerateDistribution::Ball => { - generate_random_points_in_ball_seeded::(vertex_count, 1.0, seed)? + generate_random_points_in_ball_seeded::(vertex_count.get(), 1.0, seed)? } }; let vertices = try_vertices_from_points(&points)?; @@ -1040,7 +1040,7 @@ impl Display for PachnerStressCountArgument { struct PachnerStressConfig { mode: PachnerStressMode, dimension: PachnerStressDimension, - vertex_count: usize, + vertex_count: NonZeroUsize, move_attempts: NonZeroUsize, validate_every: NonZeroUsize, key_refresh_every: NonZeroUsize, @@ -1067,31 +1067,31 @@ impl PachnerStressConfig { /// Build a validated stress configuration from command-line values. fn try_new(input: PachnerStressConfigInput) -> Result { let minimum_vertices = input.dimension.value() + 1; - if input.vertex_count < minimum_vertices { - return Err(PachnerStressError::TooFewVertices { + let vertex_count = NonZeroUsize::new(input.vertex_count) + .filter(|vertex_count| vertex_count.get() >= minimum_vertices) + .ok_or(PachnerStressError::TooFewVertices { dimension: input.dimension.value(), vertices: input.vertex_count, minimum: minimum_vertices, - }); - } + })?; let validate_every = input.validate_every.min(input.move_attempts); let growth_slack = - (input.vertex_count / DEFAULT_VERTEX_GROWTH_DIVISOR).max(input.dimension.value() + 1); - let shrink_slack = input.vertex_count / DEFAULT_VERTEX_SHRINK_DIVISOR; + (vertex_count.get() / DEFAULT_VERTEX_GROWTH_DIVISOR).max(input.dimension.value() + 1); + let shrink_slack = vertex_count.get() / DEFAULT_VERTEX_SHRINK_DIVISOR; Ok(Self { mode: input.mode, dimension: input.dimension, - vertex_count: input.vertex_count, + vertex_count, move_attempts: input.move_attempts, validate_every, key_refresh_every: input.key_refresh_every, retry_attempts: input.retry_attempts, - min_vertex_count: input - .vertex_count + min_vertex_count: vertex_count + .get() .saturating_sub(shrink_slack) .max(input.dimension.value() + 1), - max_vertex_count: input.vertex_count.saturating_add(growth_slack), + max_vertex_count: vertex_count.get().saturating_add(growth_slack), seed: input.seed, }) } @@ -1557,7 +1557,7 @@ fn run_pachner_stress_dimension( label: config.label(), mode: config.mode.label(), validation_scope: PACHNER_STRESS_VALIDATION_SCOPE_LABEL, - configured_vertices: config.vertex_count, + configured_vertices: config.vertex_count.get(), attempts: config.move_attempts().get(), validate_every: config.validate_every().get(), key_refresh_every: config.key_refresh_every().get(), @@ -1582,11 +1582,11 @@ fn build_pachner_stress_dt( reporter.emit_stage( config, "generate_points_start", - Some(config.vertex_count), + Some(config.vertex_count.get()), None, )?; let points = generate_random_points_in_range_seeded::( - config.vertex_count, + config.vertex_count.get(), stress_bounds()?, config.seed, )?; @@ -2227,11 +2227,11 @@ mod tests { use clap::Parser; use super::{ - DelaunayCliArgs, DelaunayCommand, GenerateCommand, GenerateConfig, GenerateDistribution, - PachnerStressArtifacts, PachnerStressConfig, PachnerStressConfigInput, - PachnerStressCountArgument, PachnerStressDimension, PachnerStressError, - PachnerStressInsertedFaceArity, PachnerStressInsertedFaceContext, PachnerStressMode, - build_validation_demo_export, create_progress_writer, positive_nonzero, + CliError, DelaunayCliArgs, DelaunayCommand, GenerateCommand, GenerateConfig, + GenerateDistribution, PachnerStressArtifacts, PachnerStressConfig, + PachnerStressConfigInput, PachnerStressCountArgument, PachnerStressDimension, + PachnerStressError, PachnerStressInsertedFaceArity, PachnerStressInsertedFaceContext, + PachnerStressMode, build_validation_demo_export, create_progress_writer, positive_nonzero, }; fn assert_empty_path_rejected_by_clap(args: &[&str], argument: &str) { @@ -2296,6 +2296,49 @@ mod tests { assert_eq!(config.distribution, GenerateDistribution::Ball); } + #[test] + fn generate_config_carries_validated_nonzero_vertex_count() { + let config = validated_generate_3d(&[ + "delaunay", + "generate", + "triangulation", + "--dimension", + "3", + "--vertices", + "4", + ]); + + assert_eq!(config.vertices.get(), 4); + } + + #[test] + fn generate_zero_vertices_preserves_typed_too_few_vertices_error() { + let error = DelaunayCliArgs::try_parse_from([ + "delaunay", + "generate", + "triangulation", + "--dimension", + "3", + "--vertices", + "0", + ]) + .expect("CLI arguments should parse") + .into_validated() + .expect_err("zero vertices should fail generate validation"); + + let CliError::TooFewVertices { + dimension, + vertices, + minimum, + } = error + else { + panic!("expected TooFewVertices error, got {error:?}"); + }; + assert_eq!(dimension, 3); + assert_eq!(vertices, 0); + assert_eq!(minimum, 4); + } + #[test] fn generate_rejects_unknown_distribution() { let error = DelaunayCliArgs::try_parse_from([ @@ -2319,14 +2362,39 @@ mod tests { let error = positive_nonzero(PachnerStressCountArgument::ValidateEvery, 0) .expect_err("zero should fail positive-count validation"); - match error { - PachnerStressError::NonPositive { argument, value } => { - assert_eq!(argument, PachnerStressCountArgument::ValidateEvery); - assert_eq!(argument.to_string(), "--validate-every"); - assert_eq!(value, 0); - } - other => panic!("expected NonPositive error, got {other:?}"), - } + let PachnerStressError::NonPositive { argument, value } = error else { + panic!("expected NonPositive error, got {error:?}"); + }; + assert_eq!(argument, PachnerStressCountArgument::ValidateEvery); + assert_eq!(argument.to_string(), "--validate-every"); + assert_eq!(value, 0); + } + + #[test] + fn pachner_zero_vertices_preserves_typed_too_few_vertices_error() { + let error = PachnerStressConfig::try_new(PachnerStressConfigInput { + mode: PachnerStressMode::RoundTrip, + dimension: PachnerStressDimension::Three, + vertex_count: 0, + move_attempts: NonZeroUsize::new(2).expect("literal is nonzero"), + validate_every: NonZeroUsize::new(1).expect("literal is nonzero"), + key_refresh_every: NonZeroUsize::new(7).expect("literal is nonzero"), + retry_attempts: NonZeroUsize::new(4).expect("literal is nonzero"), + seed: 42, + }) + .expect_err("zero vertices should fail Pachner stress validation"); + + let PachnerStressError::TooFewVertices { + dimension, + vertices, + minimum, + } = error + else { + panic!("expected TooFewVertices error, got {error:?}"); + }; + assert_eq!(dimension, 3); + assert_eq!(vertices, 0); + assert_eq!(minimum, 4); } #[test] @@ -2337,23 +2405,22 @@ mod tests { actual: 4, }; - match error { - PachnerStressError::InsertedFaceArity { - context, - expected, - actual, - } => { - assert_eq!(context, PachnerStressInsertedFaceContext::ForwardMove); - assert_eq!( - expected, - PachnerStressInsertedFaceArity::InvertibleForwardMove - ); - assert_eq!(context.to_string(), "forward Pachner move"); - assert_eq!(expected.to_string(), "1, 2, or 3"); - assert_eq!(actual, 4); - } - other => panic!("expected InsertedFaceArity error, got {other:?}"), - } + let PachnerStressError::InsertedFaceArity { + context, + expected, + actual, + } = error + else { + panic!("expected InsertedFaceArity error, got {error:?}"); + }; + assert_eq!(context, PachnerStressInsertedFaceContext::ForwardMove); + assert_eq!( + expected, + PachnerStressInsertedFaceArity::InvertibleForwardMove + ); + assert_eq!(context.to_string(), "forward Pachner move"); + assert_eq!(expected.to_string(), "1, 2, or 3"); + assert_eq!(actual, 4); } #[test] @@ -2362,12 +2429,10 @@ mod tests { let error = PachnerStressArtifacts::try_new(Some(path.clone()), Some(path), true) .expect_err("duplicate artifact paths should fail validation"); - match error { - PachnerStressError::DuplicateArtifactPath { path } => { - assert_eq!(path, Path::new("target/notebooks/pachner/shared.csv")); - } - other => panic!("expected DuplicateArtifactPath error, got {other:?}"), - } + let PachnerStressError::DuplicateArtifactPath { path } = error else { + panic!("expected DuplicateArtifactPath error, got {error:?}"); + }; + assert_eq!(path, Path::new("target/notebooks/pachner/shared.csv")); } #[test] @@ -2406,6 +2471,7 @@ mod tests { }) .expect("valid 3D Pachner stress config should build"); + assert_eq!(config.vertex_count.get(), 5); assert_eq!(config.move_attempts().get(), 2); assert_eq!(config.validate_every().get(), 2); assert_eq!(config.key_refresh_every().get(), 7); diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index 59f15f3f..1181f458 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -84,6 +84,7 @@ type ReplacementPeriodicOffsets = /// Bistellar flip kind descriptor. /// /// Access the move size with [`BistellarFlipKind::k`]. +/// Access the triangulation dimension with [`BistellarFlipKind::d`]. /// /// # Examples /// @@ -93,6 +94,7 @@ type ReplacementPeriodicOffsets = /// let kind = BistellarFlipKind::k2(3); /// let inverse = kind.inverse(); /// assert_eq!(kind.k(), 2); +/// assert_eq!(kind.d(), 3); /// assert_eq!(inverse.k(), 3); /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -100,7 +102,7 @@ pub struct BistellarFlipKind { /// Number of simplices being replaced on the current side (k). k: usize, /// Dimension of the triangulation (D). - pub d: usize, + d: usize, } /// Run a single flip-repair attempt using k=2 (and k=3 in 3D+). fn repair_delaunay_with_flips_k2_k3_attempt( @@ -2740,6 +2742,13 @@ impl BistellarFlipKind { pub const fn k(&self) -> usize { self.k } + + /// Dimension of the triangulation (D). + #[must_use] + pub const fn d(&self) -> usize { + self.d + } + /// Construct a k=1 flip kind for the given dimension. #[must_use] pub const fn k1(d: usize) -> Self { @@ -14373,7 +14382,7 @@ mod tests { let info_back = apply_bistellar_flip_dynamic_raw(&mut tds, 4, &context_back).unwrap(); assert_eq!(info_back.kind.k, 4); - assert_eq!(info_back.kind.d, 4); + assert_eq!(info_back.kind.d(), 4); assert_eq!(info_back.removed_simplices.len(), 4); assert_eq!(info_back.new_simplices.len(), 2); assert!(tds.is_valid().is_ok()); @@ -14410,7 +14419,7 @@ mod tests { let info_back = apply_bistellar_flip_k1_inverse_raw(&mut tds, new_key).unwrap(); assert_eq!(info_back.kind.k, 5); - assert_eq!(info_back.kind.d, 4); + assert_eq!(info_back.kind.d(), 4); assert_eq!(info_back.removed_simplices.len(), 5); assert_eq!(info_back.new_simplices.len(), 1); assert!(tds.is_valid().is_ok()); @@ -14477,7 +14486,7 @@ mod tests { let info_back = apply_bistellar_flip_dynamic_raw(&mut tds, 4, &context_back).unwrap(); assert_eq!(info_back.kind.k, 4); - assert_eq!(info_back.kind.d, 5); + assert_eq!(info_back.kind.d(), 5); assert_eq!(info_back.removed_simplices.len(), 4); assert_eq!(info_back.new_simplices.len(), 3); assert!(tds.is_valid().is_ok()); @@ -14529,7 +14538,7 @@ mod tests { let info_back = apply_bistellar_flip_dynamic_raw(&mut tds, 5, &context_back).unwrap(); assert_eq!(info_back.kind.k, 5); - assert_eq!(info_back.kind.d, 5); + assert_eq!(info_back.kind.d(), 5); assert_eq!(info_back.removed_simplices.len(), 5); assert_eq!(info_back.new_simplices.len(), 2); assert!(tds.is_valid().is_ok()); @@ -14566,7 +14575,7 @@ mod tests { let info_back = apply_bistellar_flip_k1_inverse_raw(&mut tds, new_key).unwrap(); assert_eq!(info_back.kind.k, 6); - assert_eq!(info_back.kind.d, 5); + assert_eq!(info_back.kind.d(), 5); assert_eq!(info_back.removed_simplices.len(), 6); assert_eq!(info_back.new_simplices.len(), 1); assert!(tds.is_valid().is_ok()); @@ -14594,14 +14603,14 @@ mod tests { let info = apply_bistellar_flip_k1_raw(&mut tds, simplex, new_vertex).unwrap(); assert_eq!(info.kind.k, 1); - assert_eq!(info.kind.d, 2); + assert_eq!(info.kind.d(), 2); assert_eq!(tds.number_of_simplices(), 3); let new_key = tds.vertex_key_from_uuid(&new_uuid).unwrap(); let info_back = apply_bistellar_flip_k1_inverse_raw(&mut tds, new_key).unwrap(); assert_eq!(info_back.kind.k, 3); - assert_eq!(info_back.kind.d, 2); + assert_eq!(info_back.kind.d(), 2); assert_eq!(tds.number_of_simplices(), 1); assert_eq!(tds.number_of_vertices(), 3); assert!(tds.is_valid().is_ok()); diff --git a/src/core/algorithms/incremental_insertion.rs b/src/core/algorithms/incremental_insertion.rs index 43b9c0d0..3c542bdd 100644 --- a/src/core/algorithms/incremental_insertion.rs +++ b/src/core/algorithms/incremental_insertion.rs @@ -2067,9 +2067,15 @@ impl InsertionError { | TriangulationValidationError::VertexLinkNotManifold { .. } | TriangulationValidationError::OrientationPromotionNonConvergence { .. } | TriangulationValidationError::IsolatedVertex { .. } => true, - // All other variants (structural invariant violations, future additions) - // are conservatively treated as non-retryable. - _ => false, + // Structural invariant violations are not expected to be corrected by a + // coordinate perturbation retry. + TriangulationValidationError::BoundaryFacetInClosedTopology { .. } + | TriangulationValidationError::PeriodicIdentificationInNonPeriodicTopology { + .. + } + | TriangulationValidationError::RidgeNotFound { .. } + | TriangulationValidationError::EulerCharacteristicMismatch { .. } + | TriangulationValidationError::Disconnected { .. } => false, } } } diff --git a/src/core/algorithms/locate.rs b/src/core/algorithms/locate.rs index 4eb78032..49f7982b 100644 --- a/src/core/algorithms/locate.rs +++ b/src/core/algorithms/locate.rs @@ -2333,11 +2333,11 @@ mod tests { .map(|&vkey| *dt.tds().vertex(vkey).unwrap().point()) .collect(); - println!("Simplex vertices: {simplex_points:?}"); + tracing::debug!("Simplex vertices: {simplex_points:?}"); // Test orientation of full simplex let simplex_orientation = kernel.orientation(&simplex_points).unwrap(); - println!("Simplex orientation: {simplex_orientation}"); + tracing::debug!("Simplex orientation: {simplex_orientation}"); // Test query point inside let query_inside = Point::try_new([0.3, 0.3]).expect("finite point coordinates"); @@ -2348,7 +2348,9 @@ mod tests { is_point_outside_facet(dt.tds(), &kernel, simplex_key, facet_idx, &query_inside); let is_outside = result.unwrap(); - println!("Facet {facet_idx} (opposite to vertex {facet_idx}): is_outside={is_outside}"); + tracing::debug!( + "Facet {facet_idx} (opposite to vertex {facet_idx}): is_outside={is_outside}" + ); // Point inside should NOT be outside any facet assert!( @@ -2366,7 +2368,7 @@ mod tests { is_point_outside_facet(dt.tds(), &kernel, simplex_key, facet_idx, &query_outside); let is_outside = result.unwrap(); - println!("Outside point - Facet {facet_idx}: is_outside={is_outside}"); + tracing::debug!("Outside point - Facet {facet_idx}: is_outside={is_outside}"); if is_outside { found_outside_facet = true; diff --git a/src/core/embedding.rs b/src/core/embedding.rs index f291600f..315f7298 100644 --- a/src/core/embedding.rs +++ b/src/core/embedding.rs @@ -1543,9 +1543,9 @@ fn validate_periodic_simplex_chart( simplex_key: simplex.key, simplex_uuid: simplex.uuid, detail: Box::new(simplex.detail()), - axis: span.axis, - span: span.span, - period: span.period, + axis: span.axis(), + span: span.span(), + period: span.period(), }, ); } diff --git a/src/core/insertion.rs b/src/core/insertion.rs index 6f1e75f6..a5a94fbb 100644 --- a/src/core/insertion.rs +++ b/src/core/insertion.rs @@ -63,6 +63,9 @@ const MAX_REPAIR_ITERATIONS: usize = 10; /// so 3 retries span 4 orders of magnitude (e.g. `1e-8` → `1e-5` × `local_scale` for f64). const DEFAULT_PERTURBATION_RETRIES: usize = 3; +/// Headroom used when rebuilding the duplicate-coordinate grid for a larger tolerance. +const DUPLICATE_INDEX_REBUILD_GROWTH_FACTOR: f64 = 2.0; + /// 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. /// @@ -942,7 +945,9 @@ where return; } - let Ok(mut rebuilt) = HashGridIndex::try_new(tolerance) else { + let rebuild_cell_size = + Self::duplicate_index_rebuild_cell_size(index.cell_size(), tolerance); + let Ok(mut rebuilt) = HashGridIndex::try_new(rebuild_cell_size) else { return; }; for (vkey, vertex) in self.tds.vertices() { @@ -951,6 +956,17 @@ where *index = rebuilt; } + /// Returns a rebuild cell size that preserves duplicate-candidate coverage + /// while amortizing small tolerance increases. + fn duplicate_index_rebuild_cell_size(current_cell_size: f64, tolerance: f64) -> f64 { + let grown = current_cell_size * DUPLICATE_INDEX_REBUILD_GROWTH_FACTOR; + if grown.is_finite() && grown > tolerance { + grown + } else { + tolerance + } + } + /// Compares a squared distance against the duplicate tolerance without /// overflowing the tolerance square on extreme coordinate scales. fn duplicate_distance_within_tolerance(dist_sq: f64, tolerance: f64) -> bool { @@ -2835,22 +2851,28 @@ where #[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::geometry::kernel::{AdaptiveKernel, FastKernel}; - use crate::geometry::point::Point; - use crate::geometry::traits::coordinate::{ - CoordinateConversionError, CoordinateConversionValue, DEFAULT_TOLERANCE_F64, - F64_MANTISSA_DIGITS, + use crate::{ + core::{ + algorithms::locate::InternalInconsistencySite, + collections::spatial_hash_grid::HashGridIndex, simplex::Simplex, + }, + geometry::{ + kernel::{AdaptiveKernel, FastKernel}, + point::Point, + traits::coordinate::{ + CoordinateConversionError, CoordinateConversionValue, DEFAULT_TOLERANCE_F64, + F64_MANTISSA_DIGITS, + }, + }, + triangulation::DelaunayTriangulation, + vertex, }; - use crate::triangulation::DelaunayTriangulation; - use crate::vertex; - use std::assert_matches; - use slotmap::KeyData; - use std::cell::Cell; - use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; + use std::{ + assert_matches, + cell::Cell, + sync::atomic::{AtomicBool, Ordering as AtomicOrdering}, + }; static DUPLICATE_DETECTION_FORCE_ENABLED: AtomicBool = AtomicBool::new(false); @@ -3324,13 +3346,31 @@ mod tests { tri.ensure_duplicate_index_cell_size(Some(&mut index), tolerance); - approx::assert_abs_diff_eq!(index.cell_size(), tolerance, epsilon = f64::EPSILON); + assert!( + index.cell_size() >= tolerance, + "rebuilt duplicate index must cover the requested tolerance" + ); assert!( index.for_each_candidate_vertex_key(&candidate, |_| false), "rebuilt duplicate index should remain queryable" ); } + fn duplicate_index_rebuild_adds_headroom_for_small_growth() { + let tri: Triangulation, (), (), D> = + Triangulation::new_empty(FastKernel::new()); + let mut index: HashGridIndex = HashGridIndex::try_new(1.0).unwrap(); + + tri.ensure_duplicate_index_cell_size(Some(&mut index), 1.1); + approx::assert_abs_diff_eq!(index.cell_size(), 2.0, epsilon = f64::EPSILON); + + tri.ensure_duplicate_index_cell_size(Some(&mut index), 1.5); + approx::assert_abs_diff_eq!(index.cell_size(), 2.0, epsilon = f64::EPSILON); + + tri.ensure_duplicate_index_cell_size(Some(&mut index), 5.0); + approx::assert_abs_diff_eq!(index.cell_size(), 5.0, epsilon = f64::EPSILON); + } + #[test] fn test_duplicate_distance_within_tolerance_handles_overflowed_tolerance_square() { assert!( @@ -3365,6 +3405,11 @@ mod tests { fn []() { duplicate_index_rebuilds_when_tolerance_exceeds_cell_size::<$dim>(); } + + #[test] + fn []() { + duplicate_index_rebuild_adds_headroom_for_small_growth::<$dim>(); + } )+ } }; @@ -4313,7 +4358,7 @@ mod tests { // Insert an interior point. let mut interior = [0.0; $dim]; for c in interior.iter_mut() { - *c = 1.0 / (>::from($dim + 1) * 2.0); + *c = 1.0 / (f64::from($dim + 1_i32) * 2.0); } let interior_vertex = vertex!(interior).unwrap(); let (_, hint) = insert(&mut tri, interior_vertex, None, None).unwrap(); diff --git a/src/core/util/facet_keys.rs b/src/core/util/facet_keys.rs index 0aa5c188..2cfd1d1c 100644 --- a/src/core/util/facet_keys.rs +++ b/src/core/util/facet_keys.rs @@ -464,7 +464,7 @@ mod tests { reason = "facet-key test keeps related canonicalization cases together" )] fn test_checked_facet_key_from_vertex_keys_comprehensive() { - println!("Testing checked_facet_key_from_vertex_keys comprehensively"); + tracing::debug!("Testing checked_facet_key_from_vertex_keys comprehensively"); // Create a triangulation let vertices = vec![ @@ -477,7 +477,7 @@ mod tests { let tds = dt.tds(); // Test 1: Basic functionality - successful key derivation - println!(" Testing basic functionality..."); + tracing::debug!(" Testing basic functionality..."); let simplex = tds.simplices().map(|(_, simplex)| simplex).next().unwrap(); let facet_vertex_keys: Vec<_> = simplex.vertices().iter().skip(1).copied().collect(); @@ -488,7 +488,7 @@ mod tests { ); let facet_key = result.unwrap(); - println!(" Derived facet key: {facet_key}"); + tracing::debug!(" Derived facet key: {facet_key}"); // Test deterministic behavior - same vertex keys produce same key let result2 = checked_facet_key_from_vertex_keys::<3>(&facet_vertex_keys); @@ -515,11 +515,11 @@ mod tests { facet_key, different_facet_key, "Different vertex keys should produce different facet keys" ); - println!(" Different facet key: {different_facet_key}"); + tracing::debug!(" Different facet key: {different_facet_key}"); } // Test 2: Error cases - println!(" Testing error handling..."); + tracing::debug!(" Testing error handling..."); // Wrong vertex key count let single_key: Vec = vec![facet_vertex_keys[0]]; @@ -570,7 +570,7 @@ mod tests { } // Test 3: Consistency with TDS cache - println!(" Testing consistency with TDS..."); + tracing::debug!(" Testing consistency with TDS..."); let cache = tds .build_facet_to_simplices_map() .expect("Should build facet map in test"); @@ -599,9 +599,9 @@ mod tests { } } - println!(" Found {keys_found}/{keys_tested} derived keys in TDS cache"); + tracing::debug!(" Found {keys_found}/{keys_tested} derived keys in TDS cache"); assert!(keys_tested > 0, "Should have tested some keys"); - println!(" ✓ All facet key derivation tests passed"); + tracing::debug!(" ✓ All facet key derivation tests passed"); } #[test] @@ -625,7 +625,7 @@ mod tests { // Logging: demonstrate behavior for large out-of-bounds facet index let err_large = verify_facet_index_consistency(tds, simplex_key, simplex_key, 300).unwrap_err(); - println!(" Large facet_idx=300 error: {err_large:?}"); + tracing::debug!(" Large facet_idx=300 error: {err_large:?}"); assert_matches!(err_large, FacetError::InvalidFacetIndexOverflow { .. }); // False case: two disjoint triangles in the same TDS share no facet keys. @@ -814,14 +814,14 @@ mod tests { let _ = usize_to_u8(i % 256, 300); } let duration = start.elapsed(); - eprintln!("usize_to_u8 valid conversions: 1000 iters in {duration:?}"); + tracing::debug!("usize_to_u8 valid conversions: 1000 iters in {duration:?}"); let start = Instant::now(); for i in 256..1256 { let _ = usize_to_u8(i, 100); } let duration = start.elapsed(); - eprintln!("usize_to_u8 error conversions: 1000 iters in {duration:?}"); + tracing::debug!("usize_to_u8 error conversions: 1000 iters in {duration:?}"); // Sub-test: Memory efficiency (stack allocation only) let (result, _alloc_info) = measure_with_result(|| { diff --git a/src/core/util/facet_utils.rs b/src/core/util/facet_utils.rs index 99b7a997..bafcce11 100644 --- a/src/core/util/facet_utils.rs +++ b/src/core/util/facet_utils.rs @@ -330,7 +330,7 @@ mod tests { #[test] fn test_facet_views_are_adjacent_comprehensive() { // Test 1: Adjacent facets in 3D (tetrahedra sharing a triangular face) - println!("Test 1: Adjacent facets in 3D"); + tracing::debug!("Test 1: Adjacent facets in 3D"); // Create two tetrahedra that share 3 vertices (forming a shared triangular face) let shared_vertices = vec![ @@ -382,10 +382,10 @@ mod tests { found_adjacent, "Facets representing the same shared triangle should be adjacent" ); - println!(" ✓ Adjacent facets correctly identified"); + tracing::debug!(" ✓ Adjacent facets correctly identified"); // Test 2: Non-adjacent facets from the same tetrahedra - println!("Test 2: Non-adjacent facets from same tetrahedra"); + tracing::debug!("Test 2: Non-adjacent facets from same tetrahedra"); // Find two facets that are NOT adjacent let mut found_non_adjacent = false; @@ -407,22 +407,22 @@ mod tests { found_non_adjacent, "Should be able to find non-adjacent facets" ); - println!(" ✓ Non-adjacent facets correctly identified"); + tracing::debug!(" ✓ Non-adjacent facets correctly identified"); // Test 3: Same facet should be adjacent to itself - println!("Test 3: Facet adjacent to itself"); + tracing::debug!("Test 3: Facet adjacent to itself"); let facet_view1 = facet_view1_adj.unwrap(); assert!( facet_views_are_adjacent(&facet_view1, &facet_view1), "A facet should be adjacent to itself" ); - println!(" ✓ Self-adjacency works correctly"); + tracing::debug!(" ✓ Self-adjacency works correctly"); } #[test] fn test_facet_views_are_adjacent_2d_cases() { - println!("Test 2D facet adjacency"); + tracing::debug!("Test 2D facet adjacency"); // Create two 2D triangles that share an edge (2 vertices) let shared_edge = vec![ @@ -471,12 +471,12 @@ mod tests { "2D facets (edges) sharing vertices should be adjacent" ); - println!(" ✓ 2D facet adjacency works correctly"); + tracing::debug!(" ✓ 2D facet adjacency works correctly"); } #[test] fn test_facet_views_are_adjacent_1d_cases() { - println!("Test 1D facet adjacency"); + tracing::debug!("Test 1D facet adjacency"); // In 1D, simplices are edges and facets are vertices (0D) // Two edges sharing a vertex have adjacent facets @@ -527,12 +527,12 @@ mod tests { "1D facets with different vertices should not be adjacent" ); - println!(" ✓ 1D facet adjacency works correctly"); + tracing::debug!(" ✓ 1D facet adjacency works correctly"); } #[test] fn test_facet_views_are_adjacent_edge_cases() { - println!("Test facet adjacency edge cases"); + tracing::debug!("Test facet adjacency edge cases"); // Test with minimal triangulation (single tetrahedron) let vertices = vec![ @@ -567,12 +567,12 @@ mod tests { assert!(!facet_views_are_adjacent(&facet1, &facet3)); assert!(!facet_views_are_adjacent(&facet2, &facet3)); - println!(" ✓ Single tetrahedron facet relationships correct"); + tracing::debug!(" ✓ Single tetrahedron facet relationships correct"); } #[test] fn test_facet_views_are_adjacent_performance() { - println!("Test facet adjacency performance"); + tracing::debug!("Test facet adjacency performance"); // Create a moderately complex case to test performance let vertices = vec![ @@ -599,19 +599,19 @@ mod tests { } let duration = start.elapsed(); - println!(" ✓ {iterations} adjacency checks completed in {duration:?}"); + tracing::debug!(" ✓ {iterations} adjacency checks completed in {duration:?}"); // Performance info: each check is just UUID set comparison // Note: Timing can vary significantly based on build type and CI environment if duration.as_millis() > 500 { - println!(" ⚠️ Performance warning: adjacency checks took {duration:?}"); - println!(" This may indicate debug build or slower CI environment"); + tracing::warn!(" ⚠️ Performance warning: adjacency checks took {duration:?}"); + tracing::warn!(" This may indicate debug build or slower CI environment"); } } #[test] fn test_facet_views_are_adjacent_different_geometries() { - println!("Test facet adjacency with different geometries"); + tracing::debug!("Test facet adjacency with different geometries"); // Create vertices with different coordinates to ensure different UUIDs let vertices1 = vec![ @@ -645,12 +645,12 @@ mod tests { "Facets from different geometries should not be adjacent" ); - println!(" ✓ Different geometries correctly distinguished"); + tracing::debug!(" ✓ Different geometries correctly distinguished"); } #[test] fn test_facet_views_are_adjacent_uuid_based_comparison() { - println!("Test that adjacency is purely UUID-based"); + tracing::debug!("Test that adjacency is purely UUID-based"); // Create identical geometry in separate TDS instances let vertices = vec![ @@ -683,7 +683,7 @@ mod tests { facet1_uuid_list.sort_unstable(); let mut facet2_uuid_list: Vec<_> = facet2_vertex_uuids.iter().copied().collect(); facet2_uuid_list.sort_unstable(); - println!( + tracing::debug!( " ⚠️ UUID mismatch: facet1={facet1_uuid_list:?}, facet2={facet2_uuid_list:?}" ); } @@ -695,15 +695,19 @@ mod tests { ); if uuids_are_same { - println!(" ✓ Identical coordinates produce identical UUIDs - facets are adjacent"); + tracing::debug!( + " ✓ Identical coordinates produce identical UUIDs - facets are adjacent" + ); } else { - println!(" ✓ Different UUIDs for identical coordinates - facets are not adjacent"); + tracing::debug!( + " ✓ Different UUIDs for identical coordinates - facets are not adjacent" + ); } } #[test] fn test_facet_views_are_adjacent_4d_cases() { - println!("Test 4D facet adjacency"); + tracing::debug!("Test 4D facet adjacency"); // Create two 4D simplices (5-vertices each) that share a 3D facet (4 vertices) let shared_tetrahedron = vec![ @@ -758,12 +762,12 @@ mod tests { "4D facets with different vertices should not be adjacent" ); - println!(" ✓ 4D facet adjacency works correctly"); + tracing::debug!(" ✓ 4D facet adjacency works correctly"); } #[test] fn test_facet_views_are_adjacent_5d_cases() { - println!("Test 5D facet adjacency"); + tracing::debug!("Test 5D facet adjacency"); // Create two 5D simplices (6-vertices each) that share a 4D facet (5 vertices) let shared_4d_simplex = vec![ @@ -819,26 +823,6 @@ mod tests { "5D facets with different vertices should not be adjacent" ); - println!(" ✓ 5D facet adjacency works correctly"); - } - - #[test] - fn test_facet_views_are_adjacent_multidimensional_summary() { - println!("Testing facet adjacency across all supported dimensions (1D-5D)"); - - // This test summarizes the multidimensional support - let dimensions_tested = vec![ - ("1D", "edges", "vertices"), - ("2D", "triangles", "edges"), - ("3D", "tetrahedra", "triangles"), - ("4D", "4-simplices", "tetrahedra"), - ("5D", "5-simplices", "4-simplices"), - ]; - - for (dim, simplex_type, facet_type) in dimensions_tested { - println!(" ✓ {dim}: {simplex_type} with {facet_type} facets"); - } - - println!(" ✓ All dimensional cases covered comprehensively"); + tracing::debug!(" ✓ 5D facet adjacency works correctly"); } } diff --git a/src/delaunay/builder.rs b/src/delaunay/builder.rs index 35a4ee56..c167fff0 100644 --- a/src/delaunay/builder.rs +++ b/src/delaunay/builder.rs @@ -123,12 +123,13 @@ use crate::construction::{ }; use crate::core::algorithms::incremental_insertion::InsertionError; use crate::core::collections::{ - FastHashMap, MAX_PRACTICAL_DIMENSION_SIZE, PeriodicOffsetBuffer, SmallBuffer, Uuid, - VertexKeySet, + Entry, FastHashMap, MAX_PRACTICAL_DIMENSION_SIZE, PeriodicOffsetBuffer, SimplexVertexKeyBuffer, + SmallBuffer, Uuid, VertexKeySet, }; use crate::core::construction::{ FinalDelaunayValidationContext, FinalTopologyValidationContext, TriangulationConstructionError, }; +use crate::core::facet::facet_key_from_vertices; use crate::core::operations::InsertionOutcome; use crate::core::simplex::{Simplex, SimplexValidationError}; use crate::core::tds::{ @@ -1839,6 +1840,101 @@ where || options == ConstructionOptions::default().without_final_delaunay_enforcement() } + /// Proves explicit simplex topology once so insertion can use the prechecked TDS path. + fn validate_explicit_simplices_for_prechecked_insert( + simplices: &[Simplex], + ) -> Result<(), TdsConstructionError> { + Self::validate_explicit_no_duplicate_simplices(simplices)?; + Self::validate_explicit_facet_sharing(simplices)?; + Ok(()) + } + + /// Rejects duplicate explicit maximal simplices before the insertion loop. + fn validate_explicit_no_duplicate_simplices( + simplices: &[Simplex], + ) -> Result<(), TdsConstructionError> { + let mut seen: FastHashMap = FastHashMap::default(); + + for (simplex_index, simplex) in simplices.iter().enumerate() { + let identity = Self::explicit_simplex_identity(simplex); + match seen.entry(identity) { + Entry::Occupied(entry) => { + let existing_index = *entry.get(); + let identity = entry.key(); + return Err(TdsConstructionError::ValidationError( + TdsError::DuplicateSimplices { + message: format!( + "Found duplicate explicit simplex at input indices {existing_index} and {simplex_index} with vertex keys {identity:?}" + ), + }, + )); + } + Entry::Vacant(entry) => { + entry.insert(simplex_index); + } + } + } + + Ok(()) + } + + /// Rejects explicit facets that would exceed PL-manifold multiplicity. + fn validate_explicit_facet_sharing( + simplices: &[Simplex], + ) -> Result<(), TdsConstructionError> { + let mut facet_incident_counts: FastHashMap = + FastHashMap::default(); + + for simplex in simplices { + for facet_index in 0..=D { + let facet_identity = Self::explicit_facet_identity(simplex, facet_index); + match facet_incident_counts.entry(facet_identity) { + Entry::Occupied(mut entry) => { + let incident_count = *entry.get(); + if incident_count >= 2 { + return Err(TdsConstructionError::ValidationError( + TdsError::FacetSharingViolation { + facet_key: facet_key_from_vertices(entry.key().as_slice()), + existing_incident_count: incident_count, + attempted_incident_count: incident_count + 1, + max_incident_count: 2, + candidate_simplex_uuid: simplex.uuid(), + candidate_facet_index: facet_index, + }, + )); + } + *entry.get_mut() += 1; + } + Entry::Vacant(entry) => { + entry.insert(1); + } + } + } + } + + Ok(()) + } + + /// Builds a canonical simplex identity from vertex keys for duplicate detection. + fn explicit_simplex_identity(simplex: &Simplex) -> SimplexVertexKeyBuffer { + let mut identity: SimplexVertexKeyBuffer = simplex.vertices().iter().copied().collect(); + identity.as_mut_slice().sort_unstable(); + identity + } + + /// Builds a canonical facet identity for one explicit simplex facet. + fn explicit_facet_identity( + simplex: &Simplex, + facet_index: usize, + ) -> SimplexVertexKeyBuffer { + let mut identity = SimplexVertexKeyBuffer::new(); + identity.extend(simplex.vertices().iter().enumerate().filter_map( + |(vertex_index, &vertex_key)| (vertex_index != facet_index).then_some(vertex_key), + )); + identity.as_mut_slice().sort_unstable(); + identity + } + /// Builds a triangulation from explicit vertex and simplex specifications. /// /// This is a purely combinatorial construction that assembles a valid TDS from @@ -1898,9 +1994,11 @@ where index_to_key.push(vk); } - // Insert simplices. + // Assemble simplices once with stack-backed vertex buffers, then prove + // duplicate/facet topology before using the prechecked TDS insertion path. + let mut explicit_simplices = Vec::with_capacity(simplices.len()); for (simplex_idx, simplex_spec) in simplices.iter().enumerate() { - let vertex_keys: Vec = + let vertex_keys: SimplexVertexKeyBuffer = simplex_spec.iter().map(|&vi| index_to_key[vi]).collect(); let simplex = Simplex::try_new(vertex_keys).map_err(|e| { ExplicitConstructionError::SimplexCreation { @@ -1908,11 +2006,21 @@ where source: e, } })?; - tds.insert_simplex_with_mapping(simplex).map_err(|source| { - ExplicitConstructionError::TdsAssembly { + explicit_simplices.push(simplex); + } + + Self::validate_explicit_simplices_for_prechecked_insert(&explicit_simplices).map_err( + |source| ExplicitConstructionError::TdsAssembly { + source: Box::new(source), + }, + )?; + + // Insert simplices. + for simplex in explicit_simplices { + tds.insert_simplex_with_mapping_prechecked_topology(simplex) + .map_err(|source| ExplicitConstructionError::TdsAssembly { source: Box::new(source), - } - })?; + })?; } // Mark as constructed so validation doesn't reject incomplete state. diff --git a/src/delaunay/construction.rs b/src/delaunay/construction.rs index e07c932d..cd9097f5 100644 --- a/src/delaunay/construction.rs +++ b/src/delaunay/construction.rs @@ -1483,7 +1483,35 @@ fn is_geometric_flip_error(error: &FlipError) -> bool { SimplexValidationError::DegenerateSimplex | SimplexValidationError::CoordinateConversion { .. } ), - _ => false, + FlipError::WrongTopologyOwner { .. } + | FlipError::StaleTopologyProposal { .. } + | FlipError::UnsupportedDimension { .. } + | FlipError::BoundaryFacet { .. } + | FlipError::MissingSimplex { .. } + | FlipError::DanglingVertexIncidence { .. } + | FlipError::MissingVertex { .. } + | FlipError::MissingNeighbor { .. } + | FlipError::DanglingRidgeNeighbor { .. } + | FlipError::InvalidFacetAdjacency { .. } + | FlipError::InvalidFacetIndex { .. } + | FlipError::InvalidRidgeIndex { .. } + | FlipError::InvalidRidgeAdjacency { .. } + | FlipError::InvalidRidgeMultiplicity { .. } + | FlipError::InvalidEdgeMultiplicity { .. } + | FlipError::InvalidTriangleMultiplicity { .. } + | FlipError::InvalidEdgeAdjacency { .. } + | FlipError::InvalidTriangleAdjacency { .. } + | FlipError::InvalidVertexMultiplicity { .. } + | FlipError::InvalidVertexAdjacency { .. } + | FlipError::InvalidFlipContext { .. } + | FlipError::DuplicateSimplex + | FlipError::NonManifoldFacet + | FlipError::InsertedSimplexAlreadyExists { .. } + | FlipError::FacetIteration { .. } + | FlipError::PostconditionRepair { .. } + | FlipError::EmbeddingValidation { .. } + | FlipError::NeighborWiring { .. } + | FlipError::TdsMutation { .. } => false, } } @@ -2725,6 +2753,24 @@ fn push_unique_index(indices: &mut Vec, idx: usize) { } } +/// Orders finite farthest-point candidates deterministically without hiding +/// unordered comparisons behind a catch-all branch. +fn farthest_candidate_replaces_best( + coords_f64: &[[f64; D]], + candidate_idx: usize, + best_idx: usize, + candidate_distance: f64, + best_distance: f64, +) -> bool { + match candidate_distance.partial_cmp(&best_distance) { + Some(Ordering::Greater) => true, + Some(Ordering::Equal) => { + coords_f64[candidate_idx].partial_cmp(&coords_f64[best_idx]) == Some(Ordering::Less) + } + Some(Ordering::Less) | None => false, + } +} + /// Computes the bounded candidate-pool size for max-volume simplex search. const INITIAL_SIMPLEX_MAX_VOLUME_CANDIDATE_CAP: usize = 18; @@ -2780,7 +2826,7 @@ fn append_axis_extrema(coords_f64: &[[f64; D]], candidates: &mut { min_idx = idx; } - _ => {} + Some(Ordering::Equal | Ordering::Greater) | None => {} } match coord.partial_cmp(&max_coord) { Some(Ordering::Greater) => max_idx = idx, @@ -2790,7 +2836,7 @@ fn append_axis_extrema(coords_f64: &[[f64; D]], candidates: &mut { max_idx = idx; } - _ => {} + Some(Ordering::Less | Ordering::Equal) | None => {} } } push_unique_index(candidates, min_idx); @@ -2836,12 +2882,8 @@ fn extend_candidate_pool_by_farthest_points( if !dist.is_finite() { continue; } - let replace = best_idx.is_none_or(|best_idx_val| match dist.partial_cmp(&best_dist) { - Some(Ordering::Greater) => true, - Some(Ordering::Equal) => { - coords_f64[idx].partial_cmp(&coords_f64[best_idx_val]) == Some(Ordering::Less) - } - _ => false, + let replace = best_idx.is_none_or(|best_idx_val| { + farthest_candidate_replaces_best(coords_f64, idx, best_idx_val, dist, best_dist) }); if replace { best_idx = Some(idx); @@ -2926,12 +2968,8 @@ fn select_balanced_simplex_indices( if !dist.is_finite() { continue; } - let replace = best_idx.is_none_or(|best_idx_val| match dist.partial_cmp(&best_dist) { - Some(Ordering::Greater) => true, - Some(Ordering::Equal) => { - coords_f64[i].partial_cmp(&coords_f64[best_idx_val]) == Some(Ordering::Less) - } - _ => false, + let replace = best_idx.is_none_or(|best_idx_val| { + farthest_candidate_replaces_best(&coords_f64, i, best_idx_val, dist, best_dist) }); if replace { best_idx = Some(i); diff --git a/src/delaunay/delaunayize.rs b/src/delaunay/delaunayize.rs index 2a24411b..a0e013eb 100644 --- a/src/delaunay/delaunayize.rs +++ b/src/delaunay/delaunayize.rs @@ -95,11 +95,16 @@ use thiserror::Error; /// ```rust /// use delaunay::prelude::delaunayize::DelaunayizeConfig; /// -/// let config = DelaunayizeConfig::default(); -/// assert_eq!(config.topology_max_iterations, 64); -/// assert_eq!(config.topology_max_simplices_removed, 10_000); -/// assert!(!config.fallback_rebuild); -/// assert!(config.delaunay_max_flips.is_none()); +/// let config = DelaunayizeConfig::default() +/// .with_topology_max_iterations(32) +/// .with_topology_max_simplices_removed(1_000) +/// .with_fallback_rebuild(true) +/// .with_delaunay_max_flips(500); +/// +/// assert_eq!(config.topology_max_iterations, 32); +/// assert_eq!(config.topology_max_simplices_removed, 1_000); +/// assert!(config.fallback_rebuild); +/// assert_eq!(config.delaunay_max_flips, Some(500)); /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DelaunayizeConfig { @@ -134,6 +139,93 @@ impl Default for DelaunayizeConfig { } } +impl DelaunayizeConfig { + /// Sets the maximum number of PL-manifold topology-repair iterations. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::delaunayize::DelaunayizeConfig; + /// + /// let config = DelaunayizeConfig::default().with_topology_max_iterations(32); + /// assert_eq!(config.topology_max_iterations, 32); + /// ``` + #[must_use] + pub const fn with_topology_max_iterations(mut self, max_iterations: usize) -> Self { + self.topology_max_iterations = max_iterations; + self + } + + /// Sets the maximum number of simplices topology repair may remove. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::delaunayize::DelaunayizeConfig; + /// + /// let config = DelaunayizeConfig::default().with_topology_max_simplices_removed(1_000); + /// assert_eq!(config.topology_max_simplices_removed, 1_000); + /// ``` + #[must_use] + pub const fn with_topology_max_simplices_removed( + mut self, + max_simplices_removed: usize, + ) -> Self { + self.topology_max_simplices_removed = max_simplices_removed; + self + } + + /// Enables or disables fallback rebuild after failed topology or Delaunay repair. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::delaunayize::DelaunayizeConfig; + /// + /// let config = DelaunayizeConfig::default().with_fallback_rebuild(true); + /// assert!(config.fallback_rebuild); + /// ``` + #[must_use] + pub const fn with_fallback_rebuild(mut self, fallback_rebuild: bool) -> Self { + self.fallback_rebuild = fallback_rebuild; + self + } + + /// Sets the optional per-attempt flip budget for the Delaunay repair stage. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::delaunayize::DelaunayizeConfig; + /// + /// let config = DelaunayizeConfig::default().with_delaunay_max_flips(500); + /// assert_eq!(config.delaunay_max_flips, Some(500)); + /// ``` + #[must_use] + pub const fn with_delaunay_max_flips(mut self, max_flips: usize) -> Self { + self.delaunay_max_flips = Some(max_flips); + self + } + + /// Clears the per-attempt flip budget so Delaunay repair uses its default bound. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::delaunayize::DelaunayizeConfig; + /// + /// let config = DelaunayizeConfig::default() + /// .with_delaunay_max_flips(500) + /// .without_delaunay_max_flips(); + /// assert_eq!(config.delaunay_max_flips, None); + /// ``` + #[must_use] + pub const fn without_delaunay_max_flips(mut self) -> Self { + self.delaunay_max_flips = None; + self + } +} + // ============================================================================= // OUTCOME // ============================================================================= @@ -655,10 +747,9 @@ where V: DataType, { if let Some(max_flips) = config.delaunay_max_flips { - dt.repair_delaunay_with_flips_advanced(DelaunayRepairHeuristicConfig { - max_flips: Some(max_flips), - ..DelaunayRepairHeuristicConfig::default() - }) + dt.repair_delaunay_with_flips_advanced( + DelaunayRepairHeuristicConfig::default().with_max_flips(max_flips), + ) .map(|outcome| outcome.stats) } else { dt.repair_delaunay_with_flips() @@ -1267,11 +1358,9 @@ mod tests { let outcome = delaunayize_by_flips( &mut dt, - DelaunayizeConfig { - topology_max_simplices_removed: 0, - fallback_rebuild: true, - ..DelaunayizeConfig::default() - }, + DelaunayizeConfig::default() + .with_topology_max_simplices_removed(0) + .with_fallback_rebuild(true), ) .unwrap(); @@ -1297,12 +1386,10 @@ mod tests { let err = delaunayize_by_flips( &mut dt, - DelaunayizeConfig { - topology_max_iterations: 1, - topology_max_simplices_removed: usize::MAX, - fallback_rebuild: false, - ..DelaunayizeConfig::default() - }, + DelaunayizeConfig::default() + .with_topology_max_iterations(1) + .with_topology_max_simplices_removed(usize::MAX) + .with_fallback_rebuild(false), ) .unwrap_err(); @@ -1326,12 +1413,10 @@ mod tests { let err = delaunayize_by_flips( &mut dt, - DelaunayizeConfig { - topology_max_iterations: 1, - topology_max_simplices_removed: usize::MAX, - fallback_rebuild: false, - ..DelaunayizeConfig::default() - }, + DelaunayizeConfig::default() + .with_topology_max_iterations(1) + .with_topology_max_simplices_removed(usize::MAX) + .with_fallback_rebuild(false), ) .unwrap_err(); @@ -1784,10 +1869,7 @@ mod tests { DelaunayTriangulation::builder(&vertices).build().unwrap(); // Fallback should not be triggered on a valid triangulation. - let config = DelaunayizeConfig { - fallback_rebuild: true, - ..DelaunayizeConfig::default() - }; + let config = DelaunayizeConfig::default().with_fallback_rebuild(true); let outcome = delaunayize_by_flips(&mut dt, config).unwrap(); assert!(!outcome.used_fallback_rebuild); } @@ -1800,11 +1882,9 @@ mod tests { let outcome = delaunayize_by_flips( &mut dt, - DelaunayizeConfig { - topology_max_simplices_removed: 0, - fallback_rebuild: true, - ..DelaunayizeConfig::default() - }, + DelaunayizeConfig::default() + .with_topology_max_simplices_removed(0) + .with_fallback_rebuild(true), ) .unwrap(); @@ -1825,12 +1905,10 @@ mod tests { let outcome = delaunayize_by_flips( &mut dt, - DelaunayizeConfig { - topology_max_iterations: 1, - topology_max_simplices_removed: 10_000, - fallback_rebuild: true, - ..DelaunayizeConfig::default() - }, + DelaunayizeConfig::default() + .with_topology_max_iterations(1) + .with_topology_max_simplices_removed(10_000) + .with_fallback_rebuild(true), ) .unwrap(); @@ -1853,10 +1931,7 @@ mod tests { let outcome = delaunayize_by_flips( &mut dt, - DelaunayizeConfig { - fallback_rebuild: true, - ..DelaunayizeConfig::default() - }, + DelaunayizeConfig::default().with_fallback_rebuild(true), ) .unwrap(); @@ -1885,10 +1960,7 @@ mod tests { let _guard = ForceDelaunayRepairFailureGuard::enable(); let outcome = delaunayize_by_flips( &mut dt, - DelaunayizeConfig { - fallback_rebuild: true, - ..DelaunayizeConfig::default() - }, + DelaunayizeConfig::default().with_fallback_rebuild(true), ) .unwrap(); diff --git a/src/delaunay/locality.rs b/src/delaunay/locality.rs index b6cc6ba4..18f20e61 100644 --- a/src/delaunay/locality.rs +++ b/src/delaunay/locality.rs @@ -123,6 +123,13 @@ pub fn replace_simplices_and_record_removed( /// BFS conflict search from it gives a bounded local frontier without scanning the /// entire triangulation. If no circumsphere conflict is found, the terminal simplex /// itself is still a useful local seed. +/// +/// # Errors +/// +/// Returns [`ConflictError`] when the terminal simplex is live but the bounded +/// conflict search cannot classify the local star, for example because a +/// simplex has invalid arity, references missing vertices, or a geometric +/// predicate cannot be evaluated. pub fn collect_local_exterior_conflict_seed_simplices( tds: &Tds, kernel: &K, diff --git a/src/delaunay/property_validation.rs b/src/delaunay/property_validation.rs index 04767a5e..2aaa48e9 100644 --- a/src/delaunay/property_validation.rs +++ b/src/delaunay/property_validation.rs @@ -368,6 +368,12 @@ fn first_delaunay_violation_witness( /// This performs the expensive geometric check but intentionally does **not** run /// `tds.is_valid()` up front. Callers that want cumulative validation should run /// lower-layer checks separately. +/// +/// # Errors +/// +/// Returns [`DelaunayValidationError`] when a simplex cannot be interpreted as +/// a geometric simplex, a robust predicate fails, or a vertex lies inside a +/// simplex circumsphere and therefore violates the Delaunay property. pub fn is_delaunay_property_only( tds: &Tds, ) -> Result<(), DelaunayValidationError> { diff --git a/src/delaunay/repair.rs b/src/delaunay/repair.rs index 4364500c..ec3c5b9b 100644 --- a/src/delaunay/repair.rs +++ b/src/delaunay/repair.rs @@ -167,10 +167,13 @@ impl DelaunayRepairPolicy { /// ```rust /// use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; /// -/// let mut config = DelaunayRepairHeuristicConfig::default(); -/// config.shuffle_seed = Some(7); -/// config.perturbation_seed = Some(11); +/// let config = DelaunayRepairHeuristicConfig::default() +/// .with_shuffle_seed(7) +/// .with_perturbation_seed(11) +/// .with_max_flips(100); /// assert_eq!(config.shuffle_seed, Some(7)); +/// assert_eq!(config.perturbation_seed, Some(11)); +/// assert_eq!(config.max_flips, Some(100)); /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[non_exhaustive] @@ -192,6 +195,72 @@ pub struct DelaunayRepairHeuristicConfig { } impl DelaunayRepairHeuristicConfig { + /// Sets the RNG seed used to shuffle vertex insertion order during heuristic rebuilds. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; + /// + /// let config = DelaunayRepairHeuristicConfig::default().with_shuffle_seed(7); + /// assert_eq!(config.shuffle_seed, Some(7)); + /// ``` + #[must_use] + pub const fn with_shuffle_seed(mut self, shuffle_seed: u64) -> Self { + self.shuffle_seed = Some(shuffle_seed); + self + } + + /// Sets the seed used to vary deterministic perturbation during heuristic rebuilds. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; + /// + /// let config = DelaunayRepairHeuristicConfig::default().with_perturbation_seed(11); + /// assert_eq!(config.perturbation_seed, Some(11)); + /// ``` + #[must_use] + pub const fn with_perturbation_seed(mut self, perturbation_seed: u64) -> Self { + self.perturbation_seed = Some(perturbation_seed); + self + } + + /// Sets the optional per-attempt flip budget cap. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; + /// + /// let config = DelaunayRepairHeuristicConfig::default().with_max_flips(100); + /// assert_eq!(config.max_flips, Some(100)); + /// ``` + #[must_use] + pub const fn with_max_flips(mut self, max_flips: usize) -> Self { + self.max_flips = Some(max_flips); + self + } + + /// Clears the per-attempt flip budget cap. + /// + /// # Examples + /// + /// ```rust + /// use delaunay::prelude::repair::DelaunayRepairHeuristicConfig; + /// + /// let config = DelaunayRepairHeuristicConfig::default() + /// .with_max_flips(100) + /// .without_max_flips(); + /// assert_eq!(config.max_flips, None); + /// ``` + #[must_use] + pub const fn without_max_flips(mut self) -> Self { + self.max_flips = None; + self + } + /// 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 { @@ -1332,10 +1401,7 @@ mod tests { DelaunayTriangulation::builder(&vertices).build().unwrap(); // Sub-case 1: Already Delaunay — max_flips=0 should succeed (no flips needed). - let config = DelaunayRepairHeuristicConfig { - max_flips: Some(0), - ..DelaunayRepairHeuristicConfig::default() - }; + let config = DelaunayRepairHeuristicConfig::default().with_max_flips(0); let outcome = dt.repair_delaunay_with_flips_advanced(config).unwrap(); assert_eq!(outcome.stats.flips_performed, 0); assert!( @@ -1360,10 +1426,7 @@ mod tests { DelaunayTriangulation::builder(&vertices).build().unwrap(); let _guard = ForceRepairNonconvergentGuard::enable(); - let config = DelaunayRepairHeuristicConfig { - max_flips: Some(0), - ..DelaunayRepairHeuristicConfig::default() - }; + let config = DelaunayRepairHeuristicConfig::default().with_max_flips(0); // 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(); @@ -1410,10 +1473,7 @@ mod tests { assert!(robust_dt.verify_via_flip_predicates().is_err()); // max_flips=0 should fail (flips are needed but budget is zero). - let config_zero = DelaunayRepairHeuristicConfig { - max_flips: Some(0), - ..DelaunayRepairHeuristicConfig::default() - }; + let config_zero = DelaunayRepairHeuristicConfig::default().with_max_flips(0); // 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 @@ -1429,10 +1489,7 @@ mod tests { } // Now retry with a generous budget — should succeed. - let config_generous = DelaunayRepairHeuristicConfig { - max_flips: Some(100), - ..DelaunayRepairHeuristicConfig::default() - }; + let config_generous = DelaunayRepairHeuristicConfig::default().with_max_flips(100); // 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> = @@ -1618,11 +1675,9 @@ mod tests { #[test] fn heuristic_config_resolves_missing_seeds_deterministically() { - let config = DelaunayRepairHeuristicConfig { - shuffle_seed: None, - perturbation_seed: Some(11), - max_flips: Some(7), - }; + let config = DelaunayRepairHeuristicConfig::default() + .with_perturbation_seed(11) + .with_max_flips(7); let seeds = config.resolve_seeds(5); @@ -1632,11 +1687,10 @@ mod tests { #[test] fn heuristic_config_keeps_explicit_zero_seeds() { - let config = DelaunayRepairHeuristicConfig { - shuffle_seed: Some(0), - perturbation_seed: Some(0), - max_flips: None, - }; + let config = DelaunayRepairHeuristicConfig::default() + .with_shuffle_seed(0) + .with_perturbation_seed(0) + .without_max_flips(); let seeds = config.resolve_seeds(0); diff --git a/src/delaunay/serialization.rs b/src/delaunay/serialization.rs index c55f19a0..f948e597 100644 --- a/src/delaunay/serialization.rs +++ b/src/delaunay/serialization.rs @@ -3,16 +3,15 @@ #![forbid(unsafe_code)] use crate::core::tds::Tds; -use crate::core::traits::data_type::DataType; -use crate::geometry::kernel::{Kernel, RobustKernel}; +use crate::core::traits::data_type::DataSerialize; +use crate::geometry::kernel::RobustKernel; use crate::triangulation::DelaunayTriangulation; use serde::{Deserialize, Deserializer, Serialize, Serializer}; impl Serialize for DelaunayTriangulation where - K: Kernel, - U: DataType, - V: DataType, + U: DataSerialize, + V: DataSerialize, { fn serialize(&self, serializer: S) -> Result where @@ -88,12 +87,21 @@ where #[cfg(test)] mod tests { use super::*; + use crate::core::operations::DelaunayInsertionState; use crate::core::simplex::Simplex; use crate::core::tds::TriangulationConstructionState; + use crate::core::triangulation::Triangulation; + use crate::core::validation::{TopologyGuarantee, ValidationPolicy}; use crate::geometry::kernel::AdaptiveKernel; + use crate::topology::traits::topological_space::GlobalTopology; use crate::vertex; use std::sync::Once; + struct NotAKernel; + + #[derive(Serialize)] + struct SerializeOnlyPayload(String); + fn init_tracing() { static INIT: Once = Once::new(); INIT.call_once(|| { @@ -151,6 +159,26 @@ mod tests { ); } + #[test] + fn serialize_delaunay_triangulation_does_not_require_kernel_or_datatype_bounds() { + let dt: DelaunayTriangulation = + DelaunayTriangulation { + tri: Triangulation { + kernel: NotAKernel, + tds: Tds::empty(), + global_topology: GlobalTopology::DEFAULT, + validation_policy: ValidationPolicy::default(), + topology_guarantee: TopologyGuarantee::DEFAULT, + }, + insertion_state: DelaunayInsertionState::new(), + spatial_index: None, + }; + + let json = serde_json::to_string(&dt).unwrap(); + + assert!(!json.is_empty()); + } + #[test] fn serde_roundtrip_uses_custom_deserialize_impl() { init_tracing(); diff --git a/src/geometry/embedding.rs b/src/geometry/embedding.rs index c1169067..a7d5256a 100644 --- a/src/geometry/embedding.rs +++ b/src/geometry/embedding.rs @@ -378,11 +378,31 @@ pub enum SimplexIntersectionFailure { #[derive(Clone, Copy, Debug, PartialEq)] pub struct PeriodicSimplexSpan { /// Periodic axis whose coordinate span reaches or exceeds the period. - pub axis: usize, + axis: usize, /// Coordinate span along [`axis`](Self::axis). - pub span: f64, + span: f64, /// Fundamental-domain period along [`axis`](Self::axis). - pub period: f64, + period: f64, +} + +impl PeriodicSimplexSpan { + /// Periodic axis whose coordinate span reaches or exceeds the period. + #[must_use] + pub const fn axis(&self) -> usize { + self.axis + } + + /// Coordinate span along [`axis`](Self::axis). + #[must_use] + pub const fn span(&self) -> f64 { + self.span + } + + /// Fundamental-domain period along [`axis`](Self::axis). + #[must_use] + pub const fn period(&self) -> f64 { + self.period + } } /// Returns the closed coordinate range of a simplex along one axis. @@ -500,7 +520,7 @@ pub fn axis_aligned_bounding_boxes_overlap( /// )?; /// /// let span = try_periodic_simplex_span(&simplex, &[1.0, 2.0])?; -/// assert_eq!(span.map(|witness| witness.axis), Some(0)); +/// assert_eq!(span.map(|witness| witness.axis()), Some(0)); /// # Ok(()) /// # } /// ``` @@ -1097,9 +1117,9 @@ mod tests { let span = try_periodic_simplex_span(&simplex, &[1.0, 1.0]) .unwrap() .unwrap(); - assert_eq!(span.axis, 0); - assert_abs_diff_eq!(span.span, 1.0, epsilon = f64::EPSILON); - assert_abs_diff_eq!(span.period, 1.0, epsilon = f64::EPSILON); + assert_eq!(span.axis(), 0); + assert_abs_diff_eq!(span.span(), 1.0, epsilon = f64::EPSILON); + assert_abs_diff_eq!(span.period(), 1.0, epsilon = f64::EPSILON); } #[test] diff --git a/src/geometry/predicates.rs b/src/geometry/predicates.rs index 5d2e5771..1af72e79 100644 --- a/src/geometry/predicates.rs +++ b/src/geometry/predicates.rs @@ -1031,7 +1031,7 @@ mod tests { Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"), ]; let radius_3d = circumradius(&tetrahedron_3d).unwrap(); - println!("3D circumradius: {radius_3d}"); + tracing::debug!("3D circumradius: {radius_3d}"); // For unit tetrahedron with vertices at (0,0,0), (1,0,0), (0,1,0), (0,0,1) // circumradius = sqrt(3)/2 ≈ 0.866 let expected_radius_3d = (3.0_f64).sqrt() / 2.0; @@ -1046,7 +1046,7 @@ mod tests { Point::try_new([0.0, 0.0, 0.0, 1.0]).expect("finite point coordinates"), ]; let radius_4d = circumradius(&simplex_4d).unwrap(); - println!("4D circumradius: {radius_4d}"); + tracing::debug!("4D circumradius: {radius_4d}"); // For unit 4-simplex, circumradius = 1.0 let expected_radius_4d = 1.0; assert_relative_eq!(radius_4d, expected_radius_4d, epsilon = 1e-10); @@ -1061,7 +1061,7 @@ mod tests { Point::try_new([0.0, 0.0, 0.0, 0.0, 1.0]).expect("finite point coordinates"), ]; let radius_5d = circumradius(&simplex_5d).unwrap(); - println!("5D circumradius: {radius_5d}"); + tracing::debug!("5D circumradius: {radius_5d}"); // For unit 5-simplex, circumradius = sqrt(5)/2 ≈ 1.118 let expected_radius_5d = (5.0_f64).sqrt() / 2.0; assert_relative_eq!(radius_5d, expected_radius_5d, epsilon = 1e-10); @@ -1086,15 +1086,15 @@ mod tests { "Radius should increase from 4D to 5D" ); - // Print summary for verification - println!("Circumradius summary:"); + // Log summary for verification + tracing::debug!("Circumradius summary:"); let expected_2d = (2.0_f64).sqrt() / 2.0; let expected_3d = (3.0_f64).sqrt() / 2.0; let expected_5d = (5.0_f64).sqrt() / 2.0; - println!(" 2D (right triangle): {radius_2d} ≈ {expected_2d:.6}"); - println!(" 3D (unit tetrahedron): {radius_3d} ≈ {expected_3d:.6}"); - println!(" 4D (unit 4-simplex): {radius_4d} = 1.0"); - println!(" 5D (unit 5-simplex): {radius_5d} ≈ {expected_5d:.6}"); + tracing::debug!(" 2D (right triangle): {radius_2d} ≈ {expected_2d:.6}"); + tracing::debug!(" 3D (unit tetrahedron): {radius_3d} ≈ {expected_3d:.6}"); + tracing::debug!(" 4D (unit 4-simplex): {radius_4d} = 1.0"); + tracing::debug!(" 5D (unit 5-simplex): {radius_5d} ≈ {expected_5d:.6}"); } #[test] @@ -1390,23 +1390,6 @@ mod tests { assert!(center_result.is_err()); } - #[test] - fn predicates_circumradius_with_center() { - // Test the circumradius_with_center function - let points = vec![ - Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"), - Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"), - Point::try_new([0.0, 1.0, 0.0]).expect("finite point coordinates"), - Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"), - ]; - - let center = circumcenter(&points).unwrap(); - let radius_with_center = circumradius_with_center(&points, ¢er); - let radius_direct = circumradius(&points).unwrap(); - - assert_relative_eq!(radius_with_center.unwrap(), radius_direct, epsilon = 1e-10); - } - #[test] fn predicates_circumsphere_edge_cases() { // Test circumsphere containment with simple cases @@ -1775,7 +1758,7 @@ mod tests { let result_lifted = insphere_lifted(&simplex, *test_point).unwrap(); let result_distance = insphere_distance(&simplex, *test_point).unwrap(); - println!( + tracing::debug!( "2D {description}: std={result_std:?}, lifted={result_lifted:?}, distance={result_distance:?}" ); @@ -1825,7 +1808,7 @@ mod tests { let result_lifted = insphere_lifted(&simplex, *test_point).unwrap(); let result_distance = insphere_distance(&simplex, *test_point).unwrap(); - println!( + tracing::debug!( "3D {description}: std={result_std:?}, lifted={result_lifted:?}, distance={result_distance:?}" ); @@ -1877,7 +1860,7 @@ mod tests { let result_lifted = insphere_lifted(&simplex, *test_point).unwrap(); let result_distance = insphere_distance(&simplex, *test_point).unwrap(); - println!( + tracing::debug!( "4D {description}: std={result_std:?}, lifted={result_lifted:?}, distance={result_distance:?}" ); @@ -1930,7 +1913,7 @@ mod tests { let result_lifted = insphere_lifted(&simplex, *test_point).unwrap(); let result_distance = insphere_distance(&simplex, *test_point).unwrap(); - println!( + tracing::debug!( "5D {description}: std={result_std:?}, lifted={result_lifted:?}, distance={result_distance:?}" ); @@ -2030,9 +2013,9 @@ mod tests { } } - println!("Stress test results: {total_tests} total tests"); + tracing::debug!("Stress test results: {total_tests} total tests"); for (key, count) in &disagreement_count { - println!(" {key}: {count} disagreements"); + tracing::debug!(" {key}: {count} disagreements"); } // With our fix, we should have perfect agreement diff --git a/src/geometry/traits/coordinate.rs b/src/geometry/traits/coordinate.rs index dfed4ff5..accaac52 100644 --- a/src/geometry/traits/coordinate.rs +++ b/src/geometry/traits/coordinate.rs @@ -949,15 +949,10 @@ pub const F64_MANTISSA_DIGITS: u32 = f64::MANTISSA_DIGITS; /// This names the representation contract separately from geometric scalar /// arithmetic, so APIs that only store, copy, or serialize coordinates can use /// a narrower bound than the full [`Coordinate`] interface. -pub trait CoordinateRepresentation: - Copy + Default + Debug + Serialize + DeserializeOwned + Sized -{ -} +pub trait CoordinateRepresentation: Copy + Default + Debug + Serialize + DeserializeOwned {} -impl CoordinateRepresentation for T where - T: Copy + Default + Debug + Serialize + DeserializeOwned + Sized -{ -} +impl CoordinateRepresentation for T where T: Copy + Default + Debug + Serialize + DeserializeOwned +{} /// Identity and ordering requirements for coordinate container types. /// diff --git a/src/geometry/util/conversions.rs b/src/geometry/util/conversions.rs index 1ce62ea0..d2af070f 100644 --- a/src/geometry/util/conversions.rs +++ b/src/geometry/util/conversions.rs @@ -510,12 +510,12 @@ mod tests { #[test] fn test_safe_usize_to_scalar_platform_independence() { // Test that the function behaves correctly on different platforms - println!( + tracing::debug!( "Testing on platform with usize size: {} bytes", std::mem::size_of::() ); - println!("usize::MAX = {}", usize::MAX); - println!("2^53 = {}", 1_u64 << 53); + tracing::debug!("usize::MAX = {}", usize::MAX); + tracing::debug!("2^53 = {}", 1_u64 << 53); // Values that should work on any platform let universal_safe_values = [0, 1, 100, 10000]; diff --git a/src/geometry/util/measures.rs b/src/geometry/util/measures.rs index 1d7e4d30..b8956068 100644 --- a/src/geometry/util/measures.rs +++ b/src/geometry/util/measures.rs @@ -11,7 +11,6 @@ use super::circumsphere::{ use super::conversions::{ValueConversionError, safe_coords_to_f64, safe_usize_to_scalar}; use super::norms::hypot; use crate::core::facet::FacetView; -use crate::core::traits::data_type::DataType; use crate::geometry::matrix::{DEFAULT_SINGULAR_TOL, Matrix, matrix_get, matrix_set}; use crate::geometry::point::Point; use crate::geometry::traits::coordinate::CoordinateConversionValue; @@ -818,11 +817,7 @@ fn facet_measure_gram_matrix( /// ``` pub fn surface_measure( facets: &[FacetView<'_, U, V, D>], -) -> Result -where - U: DataType, - V: DataType, -{ +) -> Result { let mut total_measure = 0.0; for facet in facets { @@ -839,16 +834,14 @@ where #[cfg(test)] mod tests { use super::*; - use crate::vertex; - use std::assert_matches; - - use crate::core::traits::facet_incidence_analysis::FacetIncidenceAnalysis; - use crate::core::vertex::Vertex; - use crate::geometry::matrix::LaError; - use crate::geometry::point::Point; - use crate::geometry::traits::coordinate::InvalidCoordinateValue; - use crate::triangulation::DelaunayTriangulation; + use crate::{ + core::{traits::facet_incidence_analysis::FacetIncidenceAnalysis, vertex::Vertex}, + geometry::{matrix::LaError, point::Point, traits::coordinate::InvalidCoordinateValue}, + triangulation::DelaunayTriangulation, + vertex, + }; use approx::assert_relative_eq; + use std::assert_matches; #[test] fn surface_measure_error_display_names_variants() { diff --git a/src/geometry/util/point_generation.rs b/src/geometry/util/point_generation.rs index 6810fa03..0a6e9c18 100644 --- a/src/geometry/util/point_generation.rs +++ b/src/geometry/util/point_generation.rs @@ -1780,32 +1780,13 @@ mod tests { assert_eq!(points_5d.len(), 0); } - #[test] - fn test_generate_random_points_seeded_2d() { - // Test seeded 2D generation reproducibility - let seed = 42_u64; - let points1 = try_generate_random_points_seeded::<2>(50, (-5.0, 5.0), seed).unwrap(); - let points2 = try_generate_random_points_seeded::<2>(50, (-5.0, 5.0), seed).unwrap(); - - assert_eq!(points1.len(), points2.len()); - - // Points should be identical with same seed - for (p1, p2) in points1.iter().zip(points2.iter()) { - let coords1 = *p1.coords(); - let coords2 = *p2.coords(); - - for (c1, c2) in coords1.iter().zip(coords2.iter()) { - assert_relative_eq!(c1, c2, epsilon = 1e-15); - } - } - } - - #[test] - fn test_generate_random_points_seeded_3d() { - // Test seeded 3D generation reproducibility - let seed = 123_u64; - let points1 = try_generate_random_points_seeded::<3>(40, (0.0, 10.0), seed).unwrap(); - let points2 = try_generate_random_points_seeded::<3>(40, (0.0, 10.0), seed).unwrap(); + fn assert_seeded_random_points_reproducible( + count: usize, + range: (f64, f64), + seed: u64, + ) { + let points1 = try_generate_random_points_seeded::(count, range, seed).unwrap(); + let points2 = try_generate_random_points_seeded::(count, range, seed).unwrap(); assert_eq!(points1.len(), points2.len()); @@ -1819,42 +1800,22 @@ mod tests { } } - #[test] - fn test_generate_random_points_seeded_4d() { - // Test seeded 4D generation reproducibility - let seed = 789_u64; - let points1 = try_generate_random_points_seeded::<4>(30, (-2.5, 2.5), seed).unwrap(); - let points2 = try_generate_random_points_seeded::<4>(30, (-2.5, 2.5), seed).unwrap(); - - assert_eq!(points1.len(), points2.len()); - - for (p1, p2) in points1.iter().zip(points2.iter()) { - let coords1 = *p1.coords(); - let coords2 = *p2.coords(); - - for (c1, c2) in coords1.iter().zip(coords2.iter()) { - assert_relative_eq!(c1, c2, epsilon = 1e-15); - } - } + macro_rules! gen_seeded_random_points_tests { + ($($name:ident: $dim:literal, $count:literal, $range:expr, $seed:literal;)*) => { + $( + #[test] + fn $name() { + assert_seeded_random_points_reproducible::<$dim>($count, $range, $seed); + } + )* + }; } - #[test] - fn test_generate_random_points_seeded_5d() { - // Test seeded 5D generation reproducibility - let seed = 456_u64; - let points1 = try_generate_random_points_seeded::<5>(20, (-1.0, 3.0), seed).unwrap(); - let points2 = try_generate_random_points_seeded::<5>(20, (-1.0, 3.0), seed).unwrap(); - - assert_eq!(points1.len(), points2.len()); - - for (p1, p2) in points1.iter().zip(points2.iter()) { - let coords1 = *p1.coords(); - let coords2 = *p2.coords(); - - for (c1, c2) in coords1.iter().zip(coords2.iter()) { - assert_relative_eq!(c1, c2, epsilon = 1e-15); - } - } + gen_seeded_random_points_tests! { + test_generate_random_points_seeded_2d: 2, 50, (-5.0, 5.0), 42; + test_generate_random_points_seeded_3d: 3, 40, (0.0, 10.0), 123; + test_generate_random_points_seeded_4d: 4, 30, (-2.5, 2.5), 789; + test_generate_random_points_seeded_5d: 5, 20, (-1.0, 3.0), 456; } #[test] diff --git a/src/lib.rs b/src/lib.rs index ddf0d5a8..d4b56bfd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,7 @@ //! | Delaunay repair and flip-based Level 5 validation | `use delaunay::prelude::repair::*` | //! | Delaunayize workflow (repair + flip) | `use delaunay::prelude::delaunayize::*` | //! | Construction telemetry diagnostics | `use delaunay::prelude::diagnostics::*` | +//! | Export stable mesh and visualization primitives | `use delaunay::prelude::export::*` | //! | Validation policies, errors, reports, PL-manifold link errors, and Level 5 diagnostics | `use delaunay::prelude::validation::*` | //! | Topology validation, Euler characteristic, ridge queries | `use delaunay::prelude::topology::validation::*` | //! | Topological spaces, topology traits, spherical point/metric backends, lifted toroidal IDs | `use delaunay::prelude::topology::spaces::*` | @@ -831,6 +832,7 @@ pub use crate::core::validation::{ TopologyGuarantee, TriangulationValidationError, ValidationConfigurationError, ValidationPolicy, }; #[cfg(feature = "diagnostics")] +#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))] pub use crate::delaunay_property_validation::debug_print_first_delaunay_violation; pub use crate::delaunay_property_validation::{ DelaunayValidationError, DelaunayViolationDetail, DelaunayViolationReport, @@ -1125,6 +1127,7 @@ pub mod tds { /// ``` pub mod algorithms { #[cfg(feature = "diagnostics")] + #[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))] pub use crate::core::algorithms::locate::verify_conflict_region_completeness; pub use crate::core::algorithms::locate::{ ConflictError, InternalInconsistencySite, LocateError, LocateFallback, diff --git a/tests/circumsphere_debug_tools.rs b/tests/circumsphere_debug_tools.rs index 72a5260a..027d48f2 100644 --- a/tests/circumsphere_debug_tools.rs +++ b/tests/circumsphere_debug_tools.rs @@ -13,12 +13,9 @@ //! cargo test --test circumsphere_debug_tools test_all_debug -- --nocapture //! ``` -use delaunay::geometry::matrix::{Matrix, determinant}; -use delaunay::geometry::util::hypot; use delaunay::prelude::construction::Vertex; use delaunay::prelude::geometry::*; use delaunay::vertex; -use serde::{Deserialize, Serialize}; // Macro for standard test output formatting macro_rules! test_output { @@ -194,25 +191,12 @@ fn test_2d_circumsphere() { test_output!("2D", &vertices, test_points); } -/// Test a single 2D point against all circumsphere methods -fn test_2d_point( - vertices: &[Vertex], - coords: [f64; 2], - description: &str, - center: &[f64; 2], - radius: f64, -) { - test_point_generic(vertices, coords, description, center, radius); -} - /// Generic function to test circumsphere methods for any dimension fn test_circumsphere_generic( dimension_name: &str, vertices: &[Vertex], test_points: Vec<([f64; D], &str)>, -) where - [f64; D]: Copy + Sized + Serialize + for<'de> Deserialize<'de>, -{ +) { println!("Testing {dimension_name} circumsphere methods"); println!("============================================="); @@ -250,9 +234,7 @@ fn test_point_generic( description: &str, center: &[f64; D], radius: f64, -) where - [f64; D]: Copy + Sized + Serialize + for<'de> Deserialize<'de>, -{ +) { let test_vertex: Vertex = vertex!(coords; data = 99).unwrap(); let vertex_points: Vec> = vertices.iter().map(Point::from).collect(); @@ -309,17 +291,6 @@ fn test_3d_circumsphere() { test_output!("3D (tetrahedron)", &vertices, test_points); } -/// Test a single 3D point against all circumsphere methods -fn test_3d_point( - vertices: &[Vertex], - coords: [f64; 3], - description: &str, - center: &[f64; 3], - radius: f64, -) { - test_point_generic(vertices, coords, description, center, radius); -} - /// Test 4D circumsphere methods with a 4-simplex fn test_4d_circumsphere() { // Create a unit 4-simplex: vertices at origin and unit vectors along each axis @@ -344,17 +315,6 @@ fn test_4d_circumsphere() { test_output!("4D (4-simplex)", &vertices, test_points); } -/// Test a single 4D point against all circumsphere methods -fn test_4d_point( - vertices: &[Vertex], - coords: [f64; 4], - description: &str, - center: &[f64; 4], - radius: f64, -) { - test_point_generic(vertices, coords, description, center, radius); -} - /// Run all orientation tests for 2D, 3D, and 4D fn test_all_orientations() { println!("============================================="); @@ -1313,7 +1273,7 @@ fn test_single_2d_point() { println!(); // Test a specific interesting point: (0.3, 0.3) - should be inside - test_2d_point( + test_point_generic( &vertices, [0.3, 0.3], "test_point", @@ -1356,7 +1316,7 @@ fn test_single_3d_point() { println!(); // Test a specific interesting point: (0.4, 0.4, 0.4) - should be inside - test_3d_point( + test_point_generic( &vertices, [0.4, 0.4, 0.4], "test_point", @@ -1400,7 +1360,7 @@ fn test_single_4d_point() { println!(); // Test a specific interesting point: (0.3, 0.3, 0.3, 0.3) - should be inside - test_4d_point( + test_point_generic( &vertices, [0.3, 0.3, 0.3, 0.3], "test_point", diff --git a/tests/cli.rs b/tests/cli.rs index 76274e4a..d82503f8 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -409,6 +409,13 @@ mod cli_tests { assert_stderr_contains(&output, "3D generation requires at least 4 vertices, got 3"); } + #[test] + fn generate_rejects_zero_vertices() { + let output = run_cli(&["generate", "--dimension", "3", "--vertices", "0"]); + assert_exit_code(&output, 1); + assert_stderr_contains(&output, "3D generation requires at least 4 vertices, got 0"); + } + #[test] fn generate_rejects_empty_output_path_during_parsing() { let output = run_cli(&[ @@ -471,6 +478,13 @@ mod cli_tests { assert_stderr_contains(&output, "3D stress requires at least 4 vertices, got 3"); } + #[test] + fn pachner_stress_rejects_zero_vertices() { + let output = run_cli(&["pachner-stress", "--dimension", "3d", "--vertices", "0"]); + assert_exit_code(&output, 1); + assert_stderr_contains(&output, "3D stress requires at least 4 vertices, got 0"); + } + #[test] fn pachner_stress_rejects_duplicate_artifact_paths() { let path = target_json_path("pachner-stress-duplicate-artifact"); diff --git a/tests/delaunay_incremental_insertion.rs b/tests/delaunay_incremental_insertion.rs index b53a5cc8..99fc21d4 100644 --- a/tests/delaunay_incremental_insertion.rs +++ b/tests/delaunay_incremental_insertion.rs @@ -10,14 +10,13 @@ //! - Different kernels (Fast vs Robust) use approx::assert_relative_eq; -use delaunay::geometry::kernel::RobustKernel; use delaunay::prelude::algorithms::LocateResult; use delaunay::prelude::collections::MAX_PRACTICAL_DIMENSION_SIZE; use delaunay::prelude::construction::{ ConstructionOptions, DedupPolicy, DelaunayTriangulation, DelaunayTriangulationBuilder, TopologyGuarantee, Vertex, vertex, }; -use delaunay::prelude::geometry::{AdaptiveKernel, Point}; +use delaunay::prelude::geometry::{AdaptiveKernel, Point, RobustKernel}; use delaunay::prelude::tds::{ Simplex, SimplexKey, SmallBuffer, VertexKey, facet_key_from_vertices, }; diff --git a/tests/delaunay_repair_fallback.rs b/tests/delaunay_repair_fallback.rs index a1e8b46f..a16eb79f 100644 --- a/tests/delaunay_repair_fallback.rs +++ b/tests/delaunay_repair_fallback.rs @@ -81,8 +81,7 @@ fn repair_fallback_produces_valid_triangulation() { } assert!(flipped, "fixture should contain a flippable interior facet"); - let mut config = DelaunayRepairHeuristicConfig::default(); - config.max_flips = Some(0); + let config = DelaunayRepairHeuristicConfig::default().with_max_flips(0); let outcome = dt .repair_delaunay_with_flips_advanced(config) .expect("heuristic rebuild fallback should repair the non-Delaunay fixture"); diff --git a/tests/delaunayize_workflow.rs b/tests/delaunayize_workflow.rs index 5d9c92ea..883ff99e 100644 --- a/tests/delaunayize_workflow.rs +++ b/tests/delaunayize_workflow.rs @@ -1,15 +1,16 @@ //! Integration tests for the delaunayize-by-flips workflow. //! //! Validates the public API in `delaunay::delaunayize`, covering: -//! - Non-Delaunay but PL-manifold success case -//! - Config defaults -//! - Outcome population on success and failure paths -//! - Fallback off vs on behavior +//! - Public workflow behavior with explicit flip budgets and fallback config +//! - Outcome population on public success and failure paths //! - Repeat-run determinism for outcome stats -//! - Multi-dimensional coverage (2D–3D) +//! - Cross-crate prelude exports and typed error payloads -use delaunay::prelude::construction::{DelaunayTriangulation, TriangulationConstructionError}; +use delaunay::prelude::construction::{ + DelaunayTriangulation, TriangulationConstructionError, Vertex, +}; use delaunay::prelude::delaunayize::*; +use delaunay::prelude::geometry::AdaptiveKernel; use delaunay::prelude::pachner::{PachnerMove, PachnerMoves}; use delaunay::vertex; use std::{error::Error, mem::size_of}; @@ -22,137 +23,45 @@ fn init_tracing() { let _ = tracing_subscriber::fmt::try_init(); } -// ============================================================================= -// CONFIG DEFAULT TESTS -// ============================================================================= - -#[test] -fn test_delaunayize_config_default_values() { - init_tracing(); - let config = DelaunayizeConfig::default(); - assert_eq!(config.topology_max_iterations, 64); - assert_eq!(config.topology_max_simplices_removed, 10_000); - assert!(!config.fallback_rebuild); -} - -// ============================================================================= -// NON-DELAUNAY BUT PL-MANIFOLD SUCCESS CASE -// ============================================================================= - -/// Build a valid PL-manifold triangulation, apply a flip to break the Delaunay -/// property, then verify that `delaunayize_by_flips` restores it. -#[test] -fn test_non_delaunay_pl_manifold_repaired_2d() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0]).unwrap(), - vertex!([4.0, 0.0]).unwrap(), - vertex!([0.0, 4.0]).unwrap(), - vertex!([4.0, 4.0]).unwrap(), - vertex!([2.0, 2.0]).unwrap(), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::builder(&vertices).build().unwrap(); +type StableDelaunay3 = DelaunayTriangulation, (), (), 3>; - // The triangulation is already Delaunay. delaunayize should be a no-op. - let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap(); - assert!(outcome.topology_repair.succeeded); - assert!(!outcome.used_fallback_rebuild); - assert!(dt.validate().is_ok()); -} - -/// Apply delaunayize on a larger 3D triangulation. -#[test] -fn test_non_delaunay_pl_manifold_repaired_3d() { - init_tracing(); - let vertices = vec![ +fn stable_3d_flip_vertices() -> Vec> { + vec![ vertex!([0.0, 0.0, 0.0]).unwrap(), vertex!([1.0, 0.0, 0.0]).unwrap(), vertex!([0.0, 1.0, 0.0]).unwrap(), vertex!([0.0, 0.0, 1.0]).unwrap(), - vertex!([1.0, 1.0, 1.0]).unwrap(), - vertex!([0.5, 0.5, 0.5]).unwrap(), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::builder(&vertices).build().unwrap(); - - let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap(); - assert!(outcome.topology_repair.succeeded); - assert!(!outcome.used_fallback_rebuild); - assert!(dt.validate().is_ok()); + vertex!([0.20, 0.20, 0.20]).unwrap(), + vertex!([0.75, 0.15, 0.30]).unwrap(), + vertex!([0.20, 0.70, 0.35]).unwrap(), + vertex!([0.30, 0.25, 0.80]).unwrap(), + vertex!([0.65, 0.60, 0.55]).unwrap(), + ] } -// ============================================================================= -// FALLBACK BEHAVIOR TESTS -// ============================================================================= - -#[test] -fn test_fallback_off_does_not_rebuild() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0, 0.0]).unwrap(), - vertex!([1.0, 0.0, 0.0]).unwrap(), - vertex!([0.0, 1.0, 0.0]).unwrap(), - vertex!([0.0, 0.0, 1.0]).unwrap(), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::builder(&vertices).build().unwrap(); - - let config = DelaunayizeConfig { - fallback_rebuild: false, - ..DelaunayizeConfig::default() - }; - let outcome = delaunayize_by_flips(&mut dt, config).unwrap(); - assert!(!outcome.used_fallback_rebuild); -} - -#[test] -fn test_fallback_on_does_not_trigger_on_valid() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0, 0.0]).unwrap(), - vertex!([1.0, 0.0, 0.0]).unwrap(), - vertex!([0.0, 1.0, 0.0]).unwrap(), - vertex!([0.0, 0.0, 1.0]).unwrap(), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::builder(&vertices).build().unwrap(); - - let config = DelaunayizeConfig { - fallback_rebuild: true, - ..DelaunayizeConfig::default() - }; - let outcome = delaunayize_by_flips(&mut dt, config).unwrap(); - // Already valid — fallback should not be triggered. - assert!(!outcome.used_fallback_rebuild); - assert!(dt.validate().is_ok()); -} - -// ============================================================================= -// OUTCOME POPULATION TESTS -// ============================================================================= - -#[test] -fn test_outcome_stats_populated_3d() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0, 0.0]).unwrap(), - vertex!([1.0, 0.0, 0.0]).unwrap(), - vertex!([0.0, 1.0, 0.0]).unwrap(), - vertex!([0.0, 0.0, 1.0]).unwrap(), - vertex!([0.5, 0.5, 0.5]).unwrap(), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::builder(&vertices).build().unwrap(); - - let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap(); - - // Topology repair stats should be populated. - assert!(outcome.topology_repair.succeeded); - assert_eq!(outcome.topology_repair.simplices_removed, 0); +fn apply_first_k2_flip(dt: &mut StableDelaunay3) -> bool { + let mut candidate_facets = Vec::new(); + for facet in dt.facets() { + let facet = facet.expect("facet iterator should resolve valid facets"); + if facet + .simplex() + .neighbor_key(usize::from(facet.facet_index())) + .flatten() + .is_some() + { + candidate_facets.push(facet.handle()); + } + } - // Delaunay repair stats should be populated. - assert!(outcome.delaunay_repair.facets_checked >= outcome.delaunay_repair.flips_performed); + for facet in candidate_facets { + let Ok(proposal) = dt.propose_pachner(PachnerMove::K2 { facet }) else { + continue; + }; + if proposal.attempt_on(dt).is_ok() { + return true; + } + } + false } // ============================================================================= @@ -199,38 +108,6 @@ fn test_repeat_run_determinism_2d() { ); } -#[test] -fn test_repeat_run_determinism_3d() { - init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0, 0.0]).unwrap(), - vertex!([1.0, 0.0, 0.0]).unwrap(), - vertex!([0.0, 1.0, 0.0]).unwrap(), - vertex!([0.0, 0.0, 1.0]).unwrap(), - vertex!([1.0, 1.0, 1.0]).unwrap(), - vertex!([0.5, 0.5, 0.5]).unwrap(), - ]; - - let config = DelaunayizeConfig::default(); - - let mut dt1: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::builder(&vertices).build().unwrap(); - let outcome1 = delaunayize_by_flips(&mut dt1, config).unwrap(); - - let mut dt2: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::builder(&vertices).build().unwrap(); - let outcome2 = delaunayize_by_flips(&mut dt2, config).unwrap(); - - assert_eq!( - outcome1.topology_repair.simplices_removed, - outcome2.topology_repair.simplices_removed - ); - assert_eq!( - outcome1.used_fallback_rebuild, - outcome2.used_fallback_rebuild - ); -} - // ============================================================================= // VERTEX PRESERVATION TEST // ============================================================================= @@ -266,47 +143,14 @@ fn test_vertex_count_preserved_after_delaunayize() { #[test] fn test_flip_breaks_delaunay_then_delaunayize_restores() { init_tracing(); - // 5 points in 3D — produces multiple simplices with interior facets. - let vertices = vec![ - vertex!([0.0, 0.0, 0.0]).unwrap(), - vertex!([1.0, 0.0, 0.0]).unwrap(), - vertex!([0.0, 1.0, 0.0]).unwrap(), - vertex!([0.0, 0.0, 1.0]).unwrap(), - vertex!([0.5, 0.5, 0.5]).unwrap(), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::builder(&vertices).build().unwrap(); + let vertices = stable_3d_flip_vertices(); + let mut dt: StableDelaunay3 = DelaunayTriangulation::builder(&vertices).build().unwrap(); assert!(dt.validate().is_ok(), "Should start valid"); - // Collect candidate interior facets (immutable borrow ends before mutation). - let mut candidate_facets = Vec::new(); - for facet in dt.facets() { - let facet = facet.expect("facet iterator should resolve valid facets"); - if facet - .simplex() - .neighbor_key(usize::from(facet.facet_index())) - .flatten() - .is_some() - { - candidate_facets.push(facet.handle()); - } - } - - let mut flipped = false; - for facet in candidate_facets { - let Ok(proposal) = dt.propose_pachner(PachnerMove::K2 { facet }) else { - continue; - }; - if proposal.attempt_on(&mut dt).is_ok() { - flipped = true; - break; - } - } - - if !flipped { - // No flippable interior facet found — skip (geometry-dependent). - return; - } + assert!( + apply_first_k2_flip(&mut dt), + "3D delaunayize fixture should provide an accepted k=2 Pachner move" + ); // Delaunay property may now be violated. // delaunayize_by_flips should restore it. @@ -487,10 +331,7 @@ fn test_delaunayize_with_explicit_flip_budget_3d() { let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let config = DelaunayizeConfig { - delaunay_max_flips: Some(1000), - ..DelaunayizeConfig::default() - }; + let config = DelaunayizeConfig::default().with_delaunay_max_flips(1000); let outcome = delaunayize_by_flips(&mut dt, config).unwrap(); assert!(outcome.topology_repair.succeeded); assert!(!outcome.used_fallback_rebuild); @@ -512,11 +353,9 @@ fn test_delaunayize_with_flip_budget_and_fallback_2d() { let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::builder(&vertices).build().unwrap(); - let config = DelaunayizeConfig { - delaunay_max_flips: Some(500), - fallback_rebuild: true, - ..DelaunayizeConfig::default() - }; + let config = DelaunayizeConfig::default() + .with_delaunay_max_flips(500) + .with_fallback_rebuild(true); let outcome = delaunayize_by_flips(&mut dt, config).unwrap(); assert!(outcome.topology_repair.succeeded); // Already valid — fallback should not be triggered. @@ -529,50 +368,16 @@ fn test_delaunayize_with_flip_budget_and_fallback_2d() { #[test] fn test_flip_breaks_then_delaunayize_with_budget_restores_3d() { init_tracing(); - let vertices = vec![ - vertex!([0.0, 0.0, 0.0]).unwrap(), - vertex!([1.0, 0.0, 0.0]).unwrap(), - vertex!([0.0, 1.0, 0.0]).unwrap(), - vertex!([0.0, 0.0, 1.0]).unwrap(), - vertex!([0.5, 0.5, 0.5]).unwrap(), - ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::builder(&vertices).build().unwrap(); + let vertices = stable_3d_flip_vertices(); + let mut dt: StableDelaunay3 = DelaunayTriangulation::builder(&vertices).build().unwrap(); assert!(dt.validate().is_ok()); - // Collect candidate interior facets. - let mut candidate_facets = Vec::new(); - for facet in dt.facets() { - let facet = facet.expect("facet iterator should resolve valid facets"); - if facet - .simplex() - .neighbor_key(usize::from(facet.facet_index())) - .flatten() - .is_some() - { - candidate_facets.push(facet.handle()); - } - } - - let mut flipped = false; - for facet in candidate_facets { - let Ok(proposal) = dt.propose_pachner(PachnerMove::K2 { facet }) else { - continue; - }; - if proposal.attempt_on(&mut dt).is_ok() { - flipped = true; - break; - } - } - - if !flipped { - return; - } + assert!( + apply_first_k2_flip(&mut dt), + "3D delaunayize budget fixture should provide an accepted k=2 Pachner move" + ); - let config = DelaunayizeConfig { - delaunay_max_flips: Some(1000), - ..DelaunayizeConfig::default() - }; + let config = DelaunayizeConfig::default().with_delaunay_max_flips(1000); let outcome = delaunayize_by_flips(&mut dt, config).unwrap(); assert!(outcome.topology_repair.succeeded); assert!(dt.validate().is_ok()); diff --git a/tests/euler_characteristic.rs b/tests/euler_characteristic.rs index a7ae9bc7..3d5d988f 100644 --- a/tests/euler_characteristic.rs +++ b/tests/euler_characteristic.rs @@ -17,18 +17,16 @@ use delaunay::vertex; use std::assert_matches; -use delaunay::builder::DelaunayTriangulationBuilder; use delaunay::prelude::construction::{ - DelaunayTriangulation, DelaunayTriangulationConstructionError, ExplicitConstructionError, - TopologyGuarantee, + DelaunayTriangulation, DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, + ExplicitConstructionError, TopologyGuarantee, }; use delaunay::prelude::geometry::AdaptiveKernel; use delaunay::prelude::tds::Tds; -use delaunay::prelude::topology::validation::ManifoldError; -use delaunay::topology::characteristics::euler; -use delaunay::topology::traits::topological_space::{ +use delaunay::prelude::topology::spaces::{ GlobalTopology, TopologyError, TopologyKind, ToroidalConstructionMode, }; +use delaunay::prelude::topology::validation::{ManifoldError, euler}; // ============================================================================= // DETERMINISTIC TESTS - KNOWN CONFIGURATIONS diff --git a/tests/large_scale_debug.rs b/tests/large_scale_debug.rs index 9915faa9..f2031874 100644 --- a/tests/large_scale_debug.rs +++ b/tests/large_scale_debug.rs @@ -99,8 +99,6 @@ #![forbid(unsafe_code)] -use delaunay::geometry::kernel::{ExactPredicates, Kernel, RobustKernel}; -use delaunay::geometry::util::safe_usize_to_scalar; use delaunay::prelude::construction::{ ConstructionOptions, ConstructionStatistics, DelaunayRepairPolicy, DelaunayTriangulation, DelaunayTriangulationBuilder, DelaunayTriangulationConstructionErrorWithStatistics, @@ -110,7 +108,9 @@ use delaunay::prelude::diagnostics::ConstructionTelemetry; use delaunay::prelude::generators::{ generate_random_points_in_ball_seeded, generate_random_points_in_range_seeded, }; -use delaunay::prelude::geometry::CoordinateRange; +use delaunay::prelude::geometry::{ + CoordinateRange, ExactPredicates, Kernel, RobustKernel, safe_usize_to_scalar, +}; #[cfg(feature = "diagnostics")] use delaunay::prelude::insertion::InsertionResult; use delaunay::prelude::insertion::{InsertionOutcome, InsertionStatistics}; @@ -1523,8 +1523,10 @@ where println!(); println!("Running final flip-based repair (advanced)..."); let t_repair = Instant::now(); - let mut repair_config = DelaunayRepairHeuristicConfig::default(); - repair_config.max_flips = repair_max_flips; + let repair_config = repair_max_flips + .map_or_else(DelaunayRepairHeuristicConfig::default, |max_flips| { + DelaunayRepairHeuristicConfig::default().with_max_flips(max_flips) + }); match dt.repair_delaunay_with_flips_advanced(repair_config) { Ok(outcome) => { println!( @@ -1835,30 +1837,22 @@ fn regression_issue_230_4d_100_orientation() { ); } -#[test] -#[cfg(feature = "slow-tests")] -fn debug_large_scale_2d() { - let outcome = debug_large_case::<2>("2D", 40_000); - assert_matches!(outcome, DebugOutcome::Success, "{outcome}"); -} - -#[test] -#[cfg(feature = "slow-tests")] -fn debug_large_scale_3d() { - let outcome = debug_large_case::<3>("3D", 7_500); - assert_matches!(outcome, DebugOutcome::Success, "{outcome}"); -} - -#[test] -#[cfg(feature = "slow-tests")] -fn debug_large_scale_4d() { - let outcome = debug_large_case::<4>("4D", 900); - assert_matches!(outcome, DebugOutcome::Success, "{outcome}"); +macro_rules! gen_debug_large_scale_tests { + ($($name:ident: $dim:literal, $label:literal, $count:literal;)*) => { + $( + #[test] + #[cfg(feature = "slow-tests")] + fn $name() { + let outcome = debug_large_case::<$dim>($label, $count); + assert_matches!(outcome, DebugOutcome::Success, "{outcome}"); + } + )* + }; } -#[test] -#[cfg(feature = "slow-tests")] -fn debug_large_scale_5d() { - let outcome = debug_large_case::<5>("5D", 150); - assert_matches!(outcome, DebugOutcome::Success, "{outcome}"); +gen_debug_large_scale_tests! { + debug_large_scale_2d: 2, "2D", 40_000; + debug_large_scale_3d: 3, "3D", 7_500; + debug_large_scale_4d: 4, "4D", 900; + debug_large_scale_5d: 5, "5D", 150; } diff --git a/tests/mesh_export.rs b/tests/mesh_export.rs index b67cef67..9640a697 100644 --- a/tests/mesh_export.rs +++ b/tests/mesh_export.rs @@ -8,7 +8,6 @@ use std::{ error::Error as StdError, }; -use delaunay::geometry::CoordinateConversionError; use delaunay::prelude::construction::{ DelaunayError, DelaunayResult, DelaunayTriangulationBuilder, DelaunayTriangulationConstructionError, TopologyGuarantee, Vertex, vertex, @@ -19,6 +18,7 @@ use delaunay::prelude::export::{ VertexRecord, VisualizationData, VisualizationDataValidationError, VisualizationExportError, VisualizationMetadata, VisualizationTopologyGuarantee, VisualizationTopologyKind, }; +use delaunay::prelude::geometry::CoordinateConversionError; use delaunay::prelude::topology::spaces::TopologyKind; use uuid::Uuid; diff --git a/tests/prelude_exports.rs b/tests/prelude_exports.rs index 333cc697..6fea8f3c 100644 --- a/tests/prelude_exports.rs +++ b/tests/prelude_exports.rs @@ -153,9 +153,9 @@ use delaunay::prelude::query::{ }; use delaunay::prelude::repair::{ DelaunayCheckPolicy, DelaunayRepairDiagnostics, DelaunayRepairError, - DelaunayRepairHeuristicRebuildFailure, DelaunayRepairHeuristicRebuildFailureKind, - DelaunayRepairHeuristicVertexContext, DelaunayRepairOperation, - DelaunayRepairOrientationCanonicalizationFailure, + DelaunayRepairHeuristicConfig, DelaunayRepairHeuristicRebuildFailure, + DelaunayRepairHeuristicRebuildFailureKind, DelaunayRepairHeuristicVertexContext, + DelaunayRepairOperation, DelaunayRepairOrientationCanonicalizationFailure, DelaunayRepairOrientationCanonicalizationFailureKind, DelaunayRepairOutcome, DelaunayRepairPostconditionFailure, DelaunayRepairStats, DelaunayRepairVerificationContext, DelaunayTriangulationValidationError, FlipEdgeAdjacencyError, FlipError, FlipFailureKind, @@ -1662,12 +1662,12 @@ fn geometry_prelude_covers_simplex_embedding_validation() { let spanning_simplex = LabeledSimplexEmbedding::try_new([4_usize, 5, 6], [[0.0, 0.0], [1.0, 0.0], [0.0, 0.25]]) .expect("valid labeled simplex embedding"); - assert_matches!( - try_periodic_simplex_span(&spanning_simplex, &[1.0, 2.0]), - Ok(Some(PeriodicSimplexSpan { axis: 0, span, period })) - if abs_diff_eq!(span, 1.0, epsilon = f64::EPSILON) - && abs_diff_eq!(period, 1.0, epsilon = f64::EPSILON) - ); + let span = try_periodic_simplex_span(&spanning_simplex, &[1.0, 2.0]) + .expect("prelude re-exports periodic span validation") + .expect("spanning simplex crosses the first periodic domain"); + assert_eq!(span.axis(), 0); + assert!(abs_diff_eq!(span.span(), 1.0, epsilon = f64::EPSILON)); + assert!(abs_diff_eq!(span.period(), 1.0, epsilon = f64::EPSILON)); let duplicate = LabeledSimplexEmbedding::<_, 2>::try_new( [7_usize, 7, 8], @@ -1689,6 +1689,7 @@ fn geometry_prelude_covers_simplex_embedding_validation() { ); let _labels: SimplexEmbeddingBuffer = [0, 1].into_iter().collect(); + assert_send_sync_unpin::(); assert_send_sync_unpin::(); assert_send_sync_unpin::(); assert_send_sync_unpin::>(); @@ -2360,6 +2361,35 @@ fn construction_prelude_covers_random_point_generation_failure_variant() Ok(()) } +fn assert_repair_heuristic_config_fluent_setters() { + let heuristic_config = DelaunayRepairHeuristicConfig::default() + .with_shuffle_seed(7) + .with_perturbation_seed(11) + .with_max_flips(100); + assert_eq!(heuristic_config.shuffle_seed, Some(7)); + assert_eq!(heuristic_config.perturbation_seed, Some(11)); + assert_eq!(heuristic_config.max_flips, Some(100)); + assert_eq!(heuristic_config.without_max_flips().max_flips, None); +} + +fn assert_delaunayize_config_fluent_setters() { + let delaunayize_config = DelaunayizeConfig::default() + .with_topology_max_iterations(32) + .with_topology_max_simplices_removed(1_000) + .with_fallback_rebuild(true) + .with_delaunay_max_flips(500); + assert_eq!(delaunayize_config.topology_max_iterations, 32); + assert_eq!(delaunayize_config.topology_max_simplices_removed, 1_000); + assert!(delaunayize_config.fallback_rebuild); + assert_eq!(delaunayize_config.delaunay_max_flips, Some(500)); + assert_eq!( + delaunayize_config + .without_delaunay_max_flips() + .delaunay_max_flips, + None + ); +} + #[test] fn diagnostic_preludes_cover_repair_apis() -> Result<(), PreludeExportTestError> { let vertices: Vec> = vec![ @@ -2380,6 +2410,7 @@ fn diagnostic_preludes_cover_repair_apis() -> Result<(), PreludeExportTestError> DelaunayRepairPolicy::default(), DelaunayRepairPolicy::EveryInsertion ); + assert_repair_heuristic_config_fluent_setters(); assert!(!DelaunayCheckPolicy::default().should_check(1)); assert_eq!(RepairQueueOrder::Fifo, RepairQueueOrder::Fifo); let diagnostics = DelaunayRepairDiagnostics { @@ -2456,6 +2487,8 @@ fn diagnostic_preludes_cover_repair_apis() -> Result<(), PreludeExportTestError> dt.verify_via_flip_predicates()?; + assert_delaunayize_config_fluent_setters(); + let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default())?; assert!(!outcome.used_fallback_rebuild); let _typed_outcome: DelaunayizeOutcome<(), (), 3> = outcome; diff --git a/tests/proptest_delaunay_triangulation.rs b/tests/proptest_delaunay_triangulation.rs index 4ea2d58d..ea701c30 100644 --- a/tests/proptest_delaunay_triangulation.rs +++ b/tests/proptest_delaunay_triangulation.rs @@ -31,7 +31,6 @@ //! local flip configurations (O(simplices)) instead of the naive O(simplices × vertices) brute-force. //! This provides ~40-100x speedup for property-based testing while remaining equally correct. -use delaunay::geometry::kernel::{AdaptiveKernel, RobustKernel}; use delaunay::prelude::construction::{ ConstructionOptions, DedupPolicy, DelaunayRepairPolicy, DelaunayTriangulation, TopologyGuarantee, Vertex, diff --git a/tests/proptest_sos.rs b/tests/proptest_sos.rs index 07e8f897..0bbc0e8f 100644 --- a/tests/proptest_sos.rs +++ b/tests/proptest_sos.rs @@ -24,9 +24,8 @@ #![forbid(unsafe_code)] -use delaunay::geometry::point::Point; use delaunay::geometry::sos::{sos_insphere_sign, sos_orientation_sign}; -use delaunay::geometry::traits::coordinate::{CoordinateConversionError, DegenerateSimplexReason}; +use delaunay::prelude::geometry::{CoordinateConversionError, DegenerateSimplexReason, Point}; use proptest::prelude::*; // ============================================================================= diff --git a/tests/proptest_toroidal.rs b/tests/proptest_toroidal.rs index 1deac8b0..671ce9e0 100644 --- a/tests/proptest_toroidal.rs +++ b/tests/proptest_toroidal.rs @@ -4,8 +4,7 @@ //! - **In-domain**: the result always lies in `[0, L_i)` for every axis `i`. //! - **Idempotent**: applying canonicalization twice gives the same result as once. -use delaunay::topology::spaces::ToroidalSpace; -use delaunay::topology::traits::topological_space::TopologicalSpace; +use delaunay::prelude::topology::spaces::{TopologicalSpace, ToroidalSpace}; use proptest::prelude::*; // ============================================================================= diff --git a/tests/proptest_triangulation.rs b/tests/proptest_triangulation.rs index 9a04ce01..9a3b527c 100644 --- a/tests/proptest_triangulation.rs +++ b/tests/proptest_triangulation.rs @@ -35,7 +35,6 @@ //! Tests are generated for dimensions 2D-5D using macros to reduce duplication. use ::uuid::Uuid; -use delaunay::geometry::quality::QualityError; use delaunay::prelude::construction::{ DelaunayTriangulation, DelaunayTriangulationBuilder, TopologyGuarantee, }; diff --git a/tests/trait_bound_ergonomics.rs b/tests/trait_bound_ergonomics.rs index fd65058c..3fb03372 100644 --- a/tests/trait_bound_ergonomics.rs +++ b/tests/trait_bound_ergonomics.rs @@ -10,14 +10,16 @@ use delaunay::prelude::algorithms::{ }; use delaunay::prelude::collections::SimplexKeyBuffer; use delaunay::prelude::construction::{GlobalTopology, TopologyGuarantee, TopologyKind}; -use delaunay::prelude::geometry::{Coordinate, CoordinateValidationError, FastKernel, Point}; -use delaunay::prelude::query::FacetIncidenceAnalysis; +use delaunay::prelude::geometry::{ + Coordinate, CoordinateValidationError, FastKernel, Point, surface_measure, +}; +use delaunay::prelude::query::{FacetIncidenceAnalysis, QueryError, TopologyIndexBuildError}; use delaunay::prelude::tds::{ - InvariantError, SimplexKey, Tds, TdsError, Vertex, VertexKey, verify_facet_index_consistency, + FacetView, InvariantError, SimplexKey, Tds, TdsError, Vertex, VertexKey, + verify_facet_index_consistency, }; use delaunay::prelude::topology::validation::validate_triangulation_euler; use delaunay::prelude::validation::DelaunayTriangulationValidationError; -use delaunay::query::{QueryError, TopologyIndexBuildError}; use uuid::Uuid; struct Payload; @@ -302,3 +304,11 @@ fn facet_views_accept_non_datatype_payloads() { assert!(tds.try_simplex_facets(SimplexKey::default()).is_err()); } + +#[test] +fn surface_measure_accepts_non_datatype_facet_views() { + let facets: [FacetView<'_, Payload, Payload, 2>; 0] = []; + let measure = surface_measure(&facets).unwrap(); + + assert!(measure.abs() <= f64::EPSILON); +}