diff --git a/benches/ci_performance_suite.rs b/benches/ci_performance_suite.rs index 735a241f..b03ec1e6 100644 --- a/benches/ci_performance_suite.rs +++ b/benches/ci_performance_suite.rs @@ -47,6 +47,11 @@ use std::{env, hint::black_box, num::NonZeroUsize, sync::Once}; #[cfg(feature = "bench-logging")] use tracing::warn; +/// Shared benchmark setup error helpers. +#[path = "common/bench_utils.rs"] +pub mod bench_utils; +use bench_utils::{abort_benchmark, bench_option, bench_result}; + /// Default point counts for 2D–4D benchmarks. const COUNTS: &[usize] = &[10, 25, 50]; /// Reduced point counts for 5D (50-point construction is prohibitively slow). @@ -65,6 +70,13 @@ type SeedSearchResult = Option<(u64, Vec>, Vec = DelaunayTriangulation, (), (), D>; type FlipTriangulation4 = DelaunayTriangulation, (), (), 4>; +fn retry_attempts(value: usize) -> NonZeroUsize { + let Some(attempts) = NonZeroUsize::new(value) else { + unreachable!("hard-coded retry attempt count must be non-zero"); + }; + attempts +} + #[derive(Clone, Copy)] enum Dataset { WellConditioned, @@ -325,41 +337,42 @@ fn prepare_data( // Slow fallback: runtime search from the base seed let base_seed = dim_seed.wrapping_add(count as u64); let search_limit = seed_search_limit(); - find_seed_vertices::(base_seed, count, bounds, search_limit, attempts).unwrap_or_else(|| { - panic!( + bench_option( + find_seed_vertices::(base_seed, count, bounds, search_limit, attempts), + format_args!( "No stable benchmark seed found for {D}D/{count}: \ start_seed={base_seed}; search_limit={search_limit}; bounds={bounds:?}" - ) - }) + ), + ) } fn prepare_dt(dim_seed: u64, count: usize) -> BenchTriangulation { let bounds = (-100.0, 100.0); - let attempts = NonZeroUsize::new(6).expect("retry attempts must be non-zero"); + let attempts = retry_attempts(6); let (seed, _, vertices) = prepare_data::(dim_seed, count, bounds, attempts); let options = ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { attempts, base_seed: Some(seed), }); - BenchTriangulation::::new_with_options(&vertices, options).unwrap_or_else(|err| { - panic!("failed to prepare {D}D benchmark triangulation with {count} vertices: {err}"); - }) + bench_result( + BenchTriangulation::::new_with_options(&vertices, options), + format!("failed to prepare {D}D benchmark triangulation with {count} vertices"), + ) } fn prepare_adv_dt(dim_seed: u64, count: usize) -> BenchTriangulation { - let attempts = NonZeroUsize::new(8).expect("retry attempts must be non-zero"); + let attempts = retry_attempts(8); let (seed, _, vertices) = prepare_adv_data::(dim_seed, count, attempts); let options = ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { attempts, base_seed: Some(seed), }); - BenchTriangulation::::new_with_options(&vertices, options).unwrap_or_else(|err| { - panic!( - "failed to prepare adversarial {D}D benchmark triangulation with {count} vertices: {err}" - ); - }) + bench_result( + BenchTriangulation::::new_with_options(&vertices, options), + format!("failed to prepare adversarial {D}D benchmark triangulation with {count} vertices"), + ) } fn prepare_inserts( @@ -372,10 +385,10 @@ fn prepare_inserts( seed ^= 0xA5A5_A5A5; } let points = match dataset { - Dataset::WellConditioned => { - generate_random_points_seeded::(count, (-50.0, 50.0), seed) - .unwrap_or_else(|error| panic!("insert point generation failed for {D}D: {error}")) - } + Dataset::WellConditioned => bench_result( + generate_random_points_seeded::(count, (-50.0, 50.0), seed), + format!("insert point generation failed for {D}D"), + ), Dataset::Adversarial => generate_adv_points::(count, seed), }; points.iter().map(|point| vertex!(*point)).collect() @@ -390,10 +403,10 @@ fn find_seed_vertices( ) -> SeedSearchResult { for offset in 0..limit { let candidate_seed = start_seed.wrapping_add(offset as u64); - let points = generate_random_points_seeded::(count, bounds, candidate_seed) - .unwrap_or_else(|error| { - panic!("generate_random_points_seeded failed for {D}D: {error}"); - }); + let points = bench_result( + generate_random_points_seeded::(count, bounds, candidate_seed), + format!("generate_random_points_seeded failed for {D}D"), + ); let vertices = points.iter().map(|p| vertex!(*p)).collect::>(); let options = ConstructionOptions::default().with_retry_policy(RetryPolicy::Shuffled { @@ -469,26 +482,29 @@ fn prepare_adv_data( } } - panic!( + abort_benchmark(format_args!( "No stable adversarial benchmark seed found for {D}D/{count}: \ start_seed={start_seed}; search_limit={search_limit}" - ); + )); } fn generate_adv_points(count: usize, seed: u64) -> Vec> { - let base_points = generate_random_points_seeded::(count, (-1.0, 1.0), seed) - .unwrap_or_else(|error| { - panic!("generate_random_points_seeded failed for adversarial {D}D: {error}"); - }); + let base_points = bench_result( + generate_random_points_seeded::(count, (-1.0, 1.0), seed), + format!("generate_random_points_seeded failed for adversarial {D}D"), + ); base_points .iter() .enumerate() .map(|(index, point)| { - let index = u32::try_from(index).expect("benchmark point index should fit in u32"); + let index = bench_result( + u32::try_from(index), + "benchmark point index should fit in u32", + ); let mut coords = [0.0_f64; D]; for (axis, coord) in coords.iter_mut().enumerate() { - let axis_number = u32::try_from(axis + 1).expect("axis should fit in u32"); + let axis_number = bench_result(u32::try_from(axis + 1), "axis should fit in u32"); let base = point.coords()[axis]; let cluster_offset = f64::from(index % 7) * 1.0e-3; let axis_offset = f64::from(axis_number) * 0.25; @@ -517,29 +533,35 @@ fn build_flip_dt_4d() -> FlipTriangulation4 { TopologyGuarantee::PLManifold, options, ) - .unwrap_or_else(|err| panic!("failed to build stable 4D flip triangulation: {err}")) + .unwrap_or_else(|err| { + abort_benchmark(format_args!( + "failed to build stable 4D flip triangulation: {err}" + )) + }) } fn cell_centroid_4d(dt: &FlipTriangulation4, cell_key: CellKey) -> [f64; 4] { - let cell = dt - .tds() - .cell(cell_key) - .expect("cell key should exist in benchmark triangulation"); + let cell = bench_option( + dt.tds().cell(cell_key), + "cell key should exist in benchmark triangulation", + ); let mut coords = [0.0_f64; 4]; for &vkey in cell.vertices() { - let vertex = dt - .tds() - .vertex(vkey) - .expect("vertex key should exist in benchmark triangulation"); + let vertex = bench_option( + dt.tds().vertex(vkey), + "vertex key should exist in benchmark triangulation", + ); let vcoords = vertex.point().coords(); for i in 0..4 { coords[i] += vcoords[i]; } } - let vertex_count = - u32::try_from(cell.vertices().len()).expect("cell vertex count should fit in u32"); + let vertex_count = bench_result( + u32::try_from(cell.vertices().len()), + "cell vertex count should fit in u32", + ); let inv = 1.0_f64 / f64::from(vertex_count); for coord in &mut coords { *coord *= inv; @@ -548,18 +570,19 @@ fn cell_centroid_4d(dt: &FlipTriangulation4, cell_key: CellKey) -> [f64; 4] { } fn cell_points_4d(dt: &FlipTriangulation4, cell_key: CellKey) -> Vec> { - let cell = dt - .tds() - .cell(cell_key) - .expect("cell key should exist in benchmark triangulation"); + let cell = bench_option( + dt.tds().cell(cell_key), + "cell key should exist in benchmark triangulation", + ); cell.vertices() .iter() .map(|vertex_key| { - *dt.tds() - .vertex(*vertex_key) - .expect("vertex key should exist in benchmark triangulation") - .point() + *bench_option( + dt.tds().vertex(*vertex_key), + "vertex key should exist in benchmark triangulation", + ) + .point() }) .collect() } @@ -572,8 +595,14 @@ fn largest_volume_cell_4d(dt: &FlipTriangulation4) -> CellKey { .map(|volume| (cell_key, volume)) }) .max_by(|(_, left), (_, right)| left.total_cmp(right)) - .map(|(cell_key, _)| cell_key) - .expect("stable 4D benchmark triangulation should have a non-degenerate cell") + .map_or_else( + || { + abort_benchmark( + "stable 4D benchmark triangulation should have a non-degenerate cell", + ) + }, + |(cell_key, _)| cell_key, + ) } fn roundtrip_k1_4d(dt: &mut FlipTriangulation4, cell_key: CellKey) { @@ -581,16 +610,20 @@ fn roundtrip_k1_4d(dt: &mut FlipTriangulation4, cell_key: CellKey) { let new_vertex = vertex!(centroid); let new_uuid = new_vertex.uuid(); - dt.flip_k1_insert(cell_key, new_vertex) - .expect("k=1 insert should succeed on stable 4D benchmark triangulation"); + bench_result( + dt.flip_k1_insert(cell_key, new_vertex), + "k=1 insert should succeed on stable 4D benchmark triangulation", + ); - let new_key = dt - .tds() - .vertex_key_from_uuid(&new_uuid) - .expect("inserted vertex should be present after k=1 insert"); + let new_key = bench_option( + dt.tds().vertex_key_from_uuid(&new_uuid), + "inserted vertex should be present after k=1 insert", + ); - dt.flip_k1_remove(new_key) - .expect("k=1 remove should invert k=1 insert"); + bench_result( + dt.flip_k1_remove(new_key), + "k=1 remove should invert k=1 insert", + ); } fn interior_facets_4d(dt: &FlipTriangulation4) -> Vec { @@ -599,7 +632,9 @@ fn interior_facets_4d(dt: &FlipTriangulation4) -> Vec { if let Some(neighbors) = cell.neighbors() { for (facet_index, neighbor) in neighbors.iter().enumerate() { if neighbor.is_some() { - let facet_index = u8::try_from(facet_index).expect("facet index fits in u8"); + let Ok(facet_index) = u8::try_from(facet_index) else { + continue; + }; facets.push(FacetHandle::new(cell_key, facet_index)); } } @@ -623,25 +658,27 @@ fn flippable_k2_facet_4d(dt: &FlipTriangulation4) -> FacetHandle { info.inserted_face_vertices[0], info.inserted_face_vertices[1], ); - trial - .flip_k2_inverse_from_edge(edge) - .expect("k=2 inverse should succeed after k=2 flip"); + bench_result( + trial.flip_k2_inverse_from_edge(edge), + "k=2 inverse should succeed after k=2 flip", + ); return facet; } Err(err) => last_error = Some(format!("{err}")), } } - panic!( + abort_benchmark(format_args!( "no flippable interior facet found for k=2 benchmark (last error: {})", last_error.unwrap_or_else(|| "none".to_string()) - ); + )); } fn roundtrip_k2_4d(dt: &mut FlipTriangulation4, facet: FacetHandle) { - let info = dt - .flip_k2(facet) - .expect("k=2 flip should succeed for preselected 4D benchmark facet"); + let info = bench_result( + dt.flip_k2(facet), + "k=2 flip should succeed for preselected 4D benchmark facet", + ); assert_eq!( info.inserted_face_vertices.len(), 2, @@ -651,8 +688,10 @@ fn roundtrip_k2_4d(dt: &mut FlipTriangulation4, facet: FacetHandle) { info.inserted_face_vertices[0], info.inserted_face_vertices[1], ); - dt.flip_k2_inverse_from_edge(edge) - .expect("k=2 inverse should succeed after k=2 flip"); + bench_result( + dt.flip_k2_inverse_from_edge(edge), + "k=2 inverse should succeed after k=2 flip", + ); } fn ridges_4d(dt: &FlipTriangulation4) -> Vec { @@ -661,8 +700,12 @@ fn ridges_4d(dt: &FlipTriangulation4) -> Vec { let vertex_count = cell.number_of_vertices(); for i in 0..vertex_count { for j in (i + 1)..vertex_count { - let omit_a = u8::try_from(i).expect("ridge index fits in u8"); - let omit_b = u8::try_from(j).expect("ridge index fits in u8"); + let Ok(omit_a) = u8::try_from(i) else { + continue; + }; + let Ok(omit_b) = u8::try_from(j) else { + continue; + }; ridges.push(RidgeHandle::new(cell_key, omit_a, omit_b)); } } @@ -686,25 +729,27 @@ fn flippable_k3_ridge_4d(dt: &FlipTriangulation4) -> RidgeHandle { info.inserted_face_vertices[1], info.inserted_face_vertices[2], ); - trial - .flip_k3_inverse_from_triangle(triangle) - .expect("k=3 inverse should succeed after k=3 flip"); + bench_result( + trial.flip_k3_inverse_from_triangle(triangle), + "k=3 inverse should succeed after k=3 flip", + ); return ridge; } Err(err) => last_error = Some(format!("{err}")), } } - panic!( + abort_benchmark(format_args!( "no flippable ridge found for k=3 benchmark (last error: {})", last_error.unwrap_or_else(|| "none".to_string()) - ); + )); } fn roundtrip_k3_4d(dt: &mut FlipTriangulation4, ridge: RidgeHandle) { - let info = dt - .flip_k3(ridge) - .expect("k=3 flip should succeed for preselected 4D benchmark ridge"); + let info = bench_result( + dt.flip_k3(ridge), + "k=3 flip should succeed for preselected 4D benchmark ridge", + ); assert_eq!( info.inserted_face_vertices.len(), 3, @@ -715,8 +760,10 @@ fn roundtrip_k3_4d(dt: &mut FlipTriangulation4, ridge: RidgeHandle) { info.inserted_face_vertices[1], info.inserted_face_vertices[2], ); - dt.flip_k3_inverse_from_triangle(triangle) - .expect("k=3 inverse should succeed after k=3 flip"); + bench_result( + dt.flip_k3_inverse_from_triangle(triangle), + "k=3 inverse should succeed after k=3 flip", + ); } fn discover_seeds_enabled() -> bool { @@ -777,8 +824,7 @@ macro_rules! benchmark_tds_new_dimension { if !filters.is_empty() && filters.iter().any(|filter| adv_bench_id.contains(filter)) { - let attempts = - NonZeroUsize::new(8).expect("retry attempts must be non-zero"); + let attempts = retry_attempts(8); let _ = prepare_adv_data::<$dim>($seed, count, attempts); return; } @@ -788,8 +834,7 @@ macro_rules! benchmark_tds_new_dimension { { let seed = ($seed as u64).wrapping_add(count as u64); let limit = seed_search_limit(); - let attempts = - NonZeroUsize::new(6).expect("retry attempts must be non-zero"); + let attempts = retry_attempts(6); if let Some((candidate_seed, _, _)) = find_seed_vertices::<$dim>(seed, count, bounds, limit, attempts) @@ -801,13 +846,13 @@ macro_rules! benchmark_tds_new_dimension { return; } - panic!( + abort_benchmark(format_args!( "seed_search_failed dim={} count={} start_seed={} limit={}", $dim, count, seed, limit - ); + )); } } @@ -831,8 +876,7 @@ macro_rules! benchmark_tds_new_dimension { // Reduce variance: pre-generate deterministic inputs outside the measured loop, // then benchmark only triangulation construction. let bounds = (-100.0, 100.0); - let attempts = - NonZeroUsize::new(6).expect("retry attempts must be non-zero"); + let attempts = retry_attempts(6); let (seed, points, vertices) = prepare_data::<$dim>($seed, count, bounds, attempts); let sample_points = points.iter().take(5).collect::>(); @@ -856,14 +900,14 @@ macro_rules! benchmark_tds_new_dimension { } Err(err) => { let error = format!("{err:?}"); - panic!( + abort_benchmark(format_args!( "DelaunayTriangulation::new failed for {}D: {error}; dim={}; count={}; seed={}; bounds={:?}; sample_points={sample_points:?}", $dim, $dim, count, seed, bounds - ); + )); } } }); @@ -873,8 +917,7 @@ macro_rules! benchmark_tds_new_dimension { BenchmarkId::new("tds_new_adversarial", count), &count, |b, &count| { - let attempts = - NonZeroUsize::new(8).expect("retry attempts must be non-zero"); + let attempts = retry_attempts(8); let (seed, points, vertices) = prepare_adv_data::<$dim>($seed, count, attempts); let sample_points = points.iter().take(5).collect::>(); @@ -895,13 +938,13 @@ macro_rules! benchmark_tds_new_dimension { } Err(err) => { let error = format!("{err:?}"); - panic!( + abort_benchmark(format_args!( "adversarial DelaunayTriangulation::new failed for {}D: {error}; dim={}; count={}; seed={}; sample_points={sample_points:?}", $dim, $dim, count, seed - ); + )); } } }); @@ -954,11 +997,13 @@ fn bench_hull_case( ), |b| { b.iter(|| { - black_box( - ConvexHull::from_triangulation(dt.as_triangulation()).unwrap_or_else(|err| { - panic!("{dimension}D convex hull extraction should succeed: {err}") - }), - ); + let hull = match ConvexHull::from_triangulation(dt.as_triangulation()) { + Ok(value) => value, + Err(error) => abort_benchmark(format_args!( + "convex hull extraction should succeed: {error}" + )), + }; + black_box(hull); }); }, ); @@ -975,10 +1020,13 @@ fn bench_validate_case( group.bench_function( BenchmarkId::new(format!("validate_{dimension}d{}", dataset.suffix()), count), |b| { - b.iter(|| { - black_box(dt.validate()).unwrap_or_else(|err| { - panic!("{dimension}D benchmark triangulation should validate: {err}"); - }); + b.iter(|| match black_box(dt.validate()) { + Ok(()) => {} + Err(error) => { + abort_benchmark(format_args!( + "{dimension}D benchmark triangulation should validate: {error}" + )); + } }); }, ); @@ -1000,9 +1048,14 @@ fn bench_insert_case( || (base_dt.clone(), insert_vertices.to_vec()), |(mut dt, vertices)| { for vertex in vertices { - black_box(dt.insert(vertex)).unwrap_or_else(|err| { - panic!("{dimension}D incremental insert should succeed: {err}"); - }); + match black_box(dt.insert(vertex)) { + Ok(_) => {} + Err(error) => { + abort_benchmark(format_args!( + "{dimension}D incremental insert should succeed: {error}" + )); + } + } } black_box(dt); }, diff --git a/benches/circumsphere_containment.rs b/benches/circumsphere_containment.rs index 9b35e58c..6d81aa75 100644 --- a/benches/circumsphere_containment.rs +++ b/benches/circumsphere_containment.rs @@ -18,6 +18,11 @@ use delaunay::prelude::generators::generate_random_points_seeded; use delaunay::prelude::query::*; use std::hint::black_box; +/// Shared benchmark setup error helpers. +#[path = "common/bench_utils.rs"] +pub mod bench_utils; +use bench_utils::{abort_benchmark, bench_option, bench_result}; + /// Generate a standard D-dimensional simplex (D+1 vertices) /// /// Creates a simplex with vertices at: @@ -41,17 +46,19 @@ fn standard_simplex() -> Vec> { /// Generate a random 3D simplex (tetrahedron) for benchmarking using seeded generation fn generate_random_simplex_3d(seed: u64) -> Vec> { - generate_random_points_seeded(4, (-10.0, 10.0), seed) - .expect("Failed to generate random simplex points") + bench_result( + generate_random_points_seeded(4, (-10.0, 10.0), seed), + "failed to generate random simplex points", + ) } /// Generate a random 3D test point using seeded generation fn generate_random_test_point_3d(seed: u64) -> Point { - generate_random_points_seeded(1, (-5.0, 5.0), seed) - .expect("Failed to generate random test point") - .into_iter() - .next() - .expect("Expected exactly one test point") + let points = bench_result( + generate_random_points_seeded(1, (-5.0, 5.0), seed), + "failed to generate random test point", + ); + bench_option(points.into_iter().next(), "expected exactly one test point") } /// Benchmark with many random queries @@ -60,13 +67,19 @@ fn benchmark_random_queries(c: &mut Criterion) { let simplex_points = generate_random_simplex_3d(42); // Generate many test points using seeded generation for reproducible results - let test_points = generate_random_points_seeded(1000, (-5.0, 5.0), 123) - .expect("Failed to generate random test points"); + let test_points = bench_result( + generate_random_points_seeded(1000, (-5.0, 5.0), 123), + "failed to generate random test points", + ); c.bench_function("random/insphere_1000_queries", |b| { b.iter(|| { for test_point in &test_points { - black_box(insphere(black_box(&simplex_points), black_box(*test_point)).unwrap()); + let result = match insphere(black_box(&simplex_points), black_box(*test_point)) { + Ok(value) => value, + Err(error) => abort_benchmark(format_args!("insphere query failed: {error}")), + }; + black_box(result); } }); }); @@ -74,9 +87,16 @@ fn benchmark_random_queries(c: &mut Criterion) { c.bench_function("random/insphere_distance_1000_queries", |b| { b.iter(|| { for test_point in &test_points { - black_box( - insphere_distance(black_box(&simplex_points), black_box(*test_point)).unwrap(), - ); + let result = + match insphere_distance(black_box(&simplex_points), black_box(*test_point)) { + Ok(value) => value, + Err(error) => { + abort_benchmark(format_args!( + "insphere_distance query failed: {error}" + )); + } + }; + black_box(result); } }); }); @@ -84,9 +104,14 @@ fn benchmark_random_queries(c: &mut Criterion) { c.bench_function("random/insphere_lifted_1000_queries", |b| { b.iter(|| { for test_point in &test_points { - black_box( - insphere_lifted(black_box(&simplex_points), black_box(*test_point)).unwrap(), - ); + let result = + match insphere_lifted(black_box(&simplex_points), black_box(*test_point)) { + Ok(value) => value, + Err(error) => { + abort_benchmark(format_args!("insphere_lifted query failed: {error}")); + } + }; + black_box(result); } }); }); @@ -96,13 +121,41 @@ fn benchmark_random_queries(c: &mut Criterion) { macro_rules! bench_simplex { ($c:ident, $dim:literal, $simplex:expr, $pt:expr) => {{ $c.bench_function(concat!($dim, "d/insphere"), |b| { - b.iter(|| black_box(insphere(black_box(&$simplex), black_box($pt)).unwrap())) + b.iter(|| { + let result = match insphere(black_box(&$simplex), black_box($pt)) { + Ok(value) => value, + Err(error) => { + abort_benchmark(format_args!("insphere benchmark query failed: {error}")); + } + }; + black_box(result) + }) }); $c.bench_function(concat!($dim, "d/insphere_distance"), |b| { - b.iter(|| black_box(insphere_distance(black_box(&$simplex), black_box($pt)).unwrap())) + b.iter(|| { + let result = match insphere_distance(black_box(&$simplex), black_box($pt)) { + Ok(value) => value, + Err(error) => { + abort_benchmark(format_args!( + "insphere_distance benchmark query failed: {error}" + )); + } + }; + black_box(result) + }) }); $c.bench_function(concat!($dim, "d/insphere_lifted"), |b| { - b.iter(|| black_box(insphere_lifted(black_box(&$simplex), black_box($pt)).unwrap())) + b.iter(|| { + let result = match insphere_lifted(black_box(&$simplex), black_box($pt)) { + Ok(value) => value, + Err(error) => { + abort_benchmark(format_args!( + "insphere_lifted benchmark query failed: {error}" + )); + } + }; + black_box(result) + }) }); }}; } @@ -112,18 +165,48 @@ macro_rules! bench_edge_case { ($c:ident, $dim:literal, $case:literal, $simplex:expr, $pt:expr) => {{ $c.bench_function( concat!("edge_cases_", $dim, "d/", $case, "_insphere"), - |b| b.iter(|| black_box(insphere(black_box(&$simplex), black_box($pt)).unwrap())), + |b| { + b.iter(|| { + let result = match insphere(black_box(&$simplex), black_box($pt)) { + Ok(value) => value, + Err(error) => { + abort_benchmark(format_args!( + "edge-case insphere benchmark query failed: {error}" + )); + } + }; + black_box(result) + }) + }, ); $c.bench_function( concat!("edge_cases_", $dim, "d/", $case, "_distance"), |b| { b.iter(|| { - black_box(insphere_distance(black_box(&$simplex), black_box($pt)).unwrap()) + let result = match insphere_distance(black_box(&$simplex), black_box($pt)) { + Ok(value) => value, + Err(error) => { + abort_benchmark(format_args!( + "edge-case insphere_distance benchmark query failed: {error}" + )); + } + }; + black_box(result) }) }, ); $c.bench_function(concat!("edge_cases_", $dim, "d/", $case, "_lifted"), |b| { - b.iter(|| black_box(insphere_lifted(black_box(&$simplex), black_box($pt)).unwrap())) + b.iter(|| { + let result = match insphere_lifted(black_box(&$simplex), black_box($pt)) { + Ok(value) => value, + Err(error) => { + abort_benchmark(format_args!( + "edge-case insphere_lifted benchmark query failed: {error}" + )); + } + }; + black_box(result) + }) }); }}; } diff --git a/benches/cold_path_predicates.rs b/benches/cold_path_predicates.rs index f52d0ee1..597c8718 100644 --- a/benches/cold_path_predicates.rs +++ b/benches/cold_path_predicates.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + //! Microbenchmark for `core::hint::cold_path` adoption in geometric predicates. //! //! This benchmark exercises the hot Stage-1 path of the [`insphere`] / [`insphere_lifted`] @@ -37,6 +39,11 @@ use delaunay::prelude::generators::generate_random_points_seeded; use delaunay::prelude::query::*; use std::hint::black_box; +/// Shared benchmark setup error helpers. +#[path = "common/bench_utils.rs"] +pub mod bench_utils; +use bench_utils::{abort_benchmark, bench_result}; + /// Deterministic seed for query-point generation in the hot path. const HOT_SEED: u64 = 0xC01D_BEEF_0000_CAFE_u64; /// Deterministic seed for query-point generation in the near-boundary group. @@ -65,8 +72,10 @@ fn standard_simplex() -> Vec> { /// Uses the range `[-10, 10]` against a unit simplex so that the Shewchuk /// errbound comfortably resolves the sign in Stage 1. fn hot_queries() -> Vec> { - generate_random_points_seeded(HOT_QUERIES, (-10.0, 10.0), HOT_SEED) - .expect("failed to generate hot-path query points") + bench_result( + generate_random_points_seeded(HOT_QUERIES, (-10.0, 10.0), HOT_SEED), + "failed to generate hot-path query points", + ) } /// Generate near-boundary query points for dimension `D`. @@ -77,21 +86,31 @@ fn near_boundary_queries() -> Vec> { // Centered near the circumsphere radius of the standard simplex (~0.5 for // the D = 3 unit case); the exact value is unimportant — we just want a // high rate of errbound-ambiguous inputs. - generate_random_points_seeded(NEAR_BOUNDARY_QUERIES, (0.40, 0.60), NEAR_BOUNDARY_SEED) - .expect("failed to generate near-boundary query points") + bench_result( + generate_random_points_seeded(NEAR_BOUNDARY_QUERIES, (0.40, 0.60), NEAR_BOUNDARY_SEED), + "failed to generate near-boundary query points", + ) } /// Run `insphere` across `queries` against `simplex`, black-boxing each result. fn run_insphere(simplex: &[Point], queries: &[Point]) { for q in queries { - black_box(insphere(black_box(simplex), black_box(*q)).unwrap()); + let result = match insphere(black_box(simplex), black_box(*q)) { + Ok(value) => value, + Err(error) => abort_benchmark(format_args!("insphere query failed: {error}")), + }; + black_box(result); } } /// Run `insphere_lifted` across `queries` against `simplex`, black-boxing each result. fn run_insphere_lifted(simplex: &[Point], queries: &[Point]) { for q in queries { - black_box(insphere_lifted(black_box(simplex), black_box(*q)).unwrap()); + let result = match insphere_lifted(black_box(simplex), black_box(*q)) { + Ok(value) => value, + Err(error) => abort_benchmark(format_args!("insphere_lifted query failed: {error}")), + }; + black_box(result); } } diff --git a/benches/common/bench_utils.rs b/benches/common/bench_utils.rs new file mode 100644 index 00000000..9d9ceef1 --- /dev/null +++ b/benches/common/bench_utils.rs @@ -0,0 +1,42 @@ +use std::{fmt::Display, process}; + +#[cfg(feature = "bench-logging")] +use std::sync::Once; +#[cfg(feature = "bench-logging")] +use tracing_subscriber::EnvFilter; + +#[cfg(feature = "bench-logging")] +fn init_tracing() { + static INIT: Once = Once::new(); + INIT.call_once(|| { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("error")); + let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init(); + }); +} + +/// Logs a benchmark setup failure when bench logging is enabled, then exits. +#[cfg(feature = "bench-logging")] +pub fn abort_benchmark(message: impl Display) -> ! { + init_tracing(); + tracing::error!("{message}"); + process::exit(1); +} + +/// Exits after a benchmark setup failure when bench logging is disabled. +#[cfg(not(feature = "bench-logging"))] +pub fn abort_benchmark(_message: impl Display) -> ! { + process::exit(1); +} + +/// Unwraps a benchmark setup result or aborts with context. +pub fn bench_result(result: Result, context: impl Display) -> T { + match result { + Ok(value) => value, + Err(error) => abort_benchmark(format_args!("{context}: {error}")), + } +} + +/// Unwraps a benchmark setup option or aborts with context. +pub fn bench_option(option: Option, context: impl Display) -> T { + option.unwrap_or_else(|| abort_benchmark(context)) +} diff --git a/benches/large_scale_performance.rs b/benches/large_scale_performance.rs index 4b0cd9dd..94a85ccf 100644 --- a/benches/large_scale_performance.rs +++ b/benches/large_scale_performance.rs @@ -85,13 +85,18 @@ use delaunay::prelude::triangulation::{ use delaunay::vertex; use std::hint::black_box; use std::num::NonZeroUsize; -use std::sync::{Mutex, OnceLock}; +use std::sync::{Mutex, Once, OnceLock}; use std::time::Duration; -use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System}; +use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System, get_current_pid}; + +/// Shared benchmark setup error helpers. +#[path = "common/bench_utils.rs"] +pub mod bench_utils; +use bench_utils::{abort_benchmark, bench_result}; #[cfg(feature = "bench-logging")] fn init_tracing() { - static INIT: std::sync::Once = std::sync::Once::new(); + static INIT: Once = Once::new(); INIT.call_once(|| { let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); @@ -131,20 +136,20 @@ struct MemoryInfo { /// Get current process memory usage in KiB fn get_memory_usage() -> u64 { static SYS: OnceLock> = OnceLock::new(); - static UNIT_LOGGED: std::sync::Once = std::sync::Once::new(); + static UNIT_LOGGED: Once = Once::new(); // Log memory unit on first call for clarity in all benchmark runs UNIT_LOGGED.call_once(|| { bench_info!("Memory measurements in KiB (sysinfo::Process::memory() / 1024)"); }); - let pid = sysinfo::get_current_pid().expect("Failed to get current PID"); + let pid = bench_result(get_current_pid(), "failed to get current PID"); let sys = SYS.get_or_init(|| { Mutex::new(System::new_with_specifics( RefreshKind::nothing().with_processes(ProcessRefreshKind::nothing().with_memory()), )) }); - let mut system = sys.lock().expect("lock System"); + let mut system = bench_result(sys.lock(), "failed to lock System"); system.refresh_processes_specifics( ProcessesToUpdate::Some(&[pid]), true, @@ -189,7 +194,10 @@ fn benchmark_retry_attempts() -> NonZeroUsize { .unwrap_or(6) .max(1); - NonZeroUsize::new(attempts).expect("attempts clamped to >= 1") + let Some(attempts) = NonZeroUsize::new(attempts) else { + unreachable!("attempts clamped to >= 1"); + }; + attempts }) } @@ -213,13 +221,12 @@ fn construct_triangulation( vertices: &[Vertex], seed: u64, ) -> DelaunayTriangulation, (), (), D> { - DelaunayTriangulation::new_with_options(vertices, construction_options(seed)).unwrap_or_else( - |err| { - panic!( - "Failed to create triangulation (dim={D}, n_vertices={}, seed={seed}): {err:?}", - vertices.len() - ) - }, + bench_result( + DelaunayTriangulation::new_with_options(vertices, construction_options(seed)), + format!( + "failed to create triangulation (dim={D}, n_vertices={}, seed={seed})", + vertices.len() + ), ) } @@ -228,8 +235,10 @@ fn measure_construction_with_memory(n_points: usize, seed: u64) let mem_before = get_memory_usage(); // Generate points and vertices (setup overhead) - let points = generate_random_points_seeded::(n_points, (-100.0, 100.0), seed) - .expect("Failed to generate points"); + let points = bench_result( + generate_random_points_seeded::(n_points, (-100.0, 100.0), seed), + "failed to generate points", + ); let vertices: Vec<_> = points.into_iter().map(|p| vertex!(p)).collect(); // Measure memory before triangulation construction to isolate allocation @@ -288,9 +297,10 @@ fn bench_construction(c: &mut Criterion, dimension_name: &str, n b.iter_batched( || { // Setup: Generate points (not measured) - let points = - generate_random_points_seeded::(n_points, (-100.0, 100.0), seed) - .expect("Failed to generate points"); + let points = bench_result( + generate_random_points_seeded::(n_points, (-100.0, 100.0), seed), + "failed to generate points", + ); points.into_iter().map(|p| vertex!(p)).collect::>() }, |vertices| { @@ -362,8 +372,10 @@ fn bench_validation(c: &mut Criterion, dimension_name: &str, n_p } // Pre-generate triangulation for validation benchmarks - let points = generate_random_points_seeded::(n_points, (-100.0, 100.0), seed) - .expect("Failed to generate points"); + let points = bench_result( + generate_random_points_seeded::(n_points, (-100.0, 100.0), seed), + "failed to generate points", + ); let vertices: Vec<_> = points.into_iter().map(|p| vertex!(p)).collect(); let dt = construct_triangulation::(&vertices, seed); let tri = dt.as_triangulation(); @@ -374,8 +386,11 @@ fn bench_validation(c: &mut Criterion, dimension_name: &str, n_p group.bench_function("validate_topology", |b| { b.iter(|| { // Level 3 topology check (manifold-with-boundary + Euler characteristic) - tri.is_valid() - .expect("triangulation should be structurally valid during validation benchmark"); + if let Err(error) = tri.is_valid() { + abort_benchmark(format_args!( + "triangulation should be structurally valid during validation benchmark: {error}" + )); + } }); }); @@ -404,8 +419,10 @@ fn bench_neighbor_queries( let seed = seed_for_case::(n_points); // Pre-generate triangulation - let points = generate_random_points_seeded::(n_points, (-100.0, 100.0), seed) - .expect("Failed to generate points"); + let points = bench_result( + generate_random_points_seeded::(n_points, (-100.0, 100.0), seed), + "failed to generate points", + ); let vertices: Vec<_> = points.into_iter().map(|p| vertex!(p)).collect(); let dt = construct_triangulation::(&vertices, seed); let tds = dt.tds(); @@ -448,8 +465,10 @@ fn bench_vertex_iteration( let seed = seed_for_case::(n_points); // Pre-generate triangulation - let points = generate_random_points_seeded::(n_points, (-100.0, 100.0), seed) - .expect("Failed to generate points"); + let points = bench_result( + generate_random_points_seeded::(n_points, (-100.0, 100.0), seed), + "failed to generate points", + ); let vertices: Vec<_> = points.into_iter().map(|p| vertex!(p)).collect(); let dt = construct_triangulation::(&vertices, seed); let tds = dt.tds(); @@ -483,8 +502,10 @@ fn bench_cell_iteration(c: &mut Criterion, dimension_name: &str, let seed = seed_for_case::(n_points); // Pre-generate triangulation - let points = generate_random_points_seeded::(n_points, (-100.0, 100.0), seed) - .expect("Failed to generate points"); + let points = bench_result( + generate_random_points_seeded::(n_points, (-100.0, 100.0), seed), + "failed to generate points", + ); let vertices: Vec<_> = points.into_iter().map(|p| vertex!(p)).collect(); let dt = construct_triangulation::(&vertices, seed); let tds = dt.tds(); diff --git a/benches/profiling_suite.rs b/benches/profiling_suite.rs index 957f5dbc..bee63b42 100644 --- a/benches/profiling_suite.rs +++ b/benches/profiling_suite.rs @@ -75,6 +75,18 @@ use std::num::NonZeroUsize; use std::sync::Once; use std::time::{Duration, Instant}; +/// Shared benchmark setup error helpers. +#[path = "common/bench_utils.rs"] +pub mod bench_utils; +use bench_utils::{abort_benchmark, bench_result}; + +fn retry_attempts(value: usize) -> NonZeroUsize { + let Some(attempts) = NonZeroUsize::new(value) else { + unreachable!("hard-coded retry attempt count must be non-zero"); + }; + attempts +} + #[cfg(feature = "bench-logging")] fn init_tracing() { static INIT: Once = Once::new(); @@ -201,21 +213,28 @@ fn gen_points( seed: u64, ) -> Vec> { match distribution { - PointDistribution::Random => generate_random_points_seeded(count, (-100.0, 100.0), seed) - .expect("random point generation failed"), - PointDistribution::Adversarial => generate_random_points_seeded::( - count, - (-1.0, 1.0), - seed ^ 0xA5A5_A5A5_A5A5_A5A5, + PointDistribution::Random => bench_result( + generate_random_points_seeded(count, (-100.0, 100.0), seed), + "random point generation failed", + ), + PointDistribution::Adversarial => bench_result( + generate_random_points_seeded::( + count, + (-1.0, 1.0), + seed ^ 0xA5A5_A5A5_A5A5_A5A5, + ), + "adversarial base point generation failed", ) - .expect("adversarial base point generation failed") .iter() .enumerate() .map(|(index, point)| { - let index = u32::try_from(index).expect("benchmark point index should fit in u32"); + let index = bench_result( + u32::try_from(index), + "benchmark point index should fit in u32", + ); let mut coords = [0.0_f64; D]; for (axis, coord) in coords.iter_mut().enumerate() { - let axis_number = u32::try_from(axis + 1).expect("axis should fit in u32"); + let axis_number = bench_result(u32::try_from(axis + 1), "axis should fit in u32"); let base: f64 = point.coords()[axis]; let cluster_offset = f64::from(index % 7) * 1.0e-3; let axis_offset = f64::from(axis_number) * 0.25; @@ -243,10 +262,10 @@ fn gen_points( // Grid generation failed - this indicates a configuration issue // Rather than silently falling back and producing misleading benchmarks, // we should fail fast to alert developers to adjust parameters - panic!( + abort_benchmark(format_args!( "Grid generation failed for D={D}: count={count}, points_per_dim={points_per_dim}, err={e:?}. \ Adjust grid parameters or use smaller point counts for high-dimensional grid benchmarks." - ); + )); } } } @@ -258,8 +277,10 @@ fn gen_points( 5 => 15.0, // 5D: even larger spacing _ => 20.0, // Higher dimensions: very large spacing }; - generate_poisson_points(count, (-100.0, 100.0), min_distance, seed) - .expect("poisson point generation failed") + bench_result( + generate_poisson_points(count, (-100.0, 100.0), min_distance, seed), + "poisson point generation failed", + ) } } } @@ -819,8 +840,7 @@ macro_rules! benchmark_validation_components_dimension { let vertices: Vec<_> = points.iter().map(|point| vertex!(*point)).collect(); let builder = DelaunayTriangulationBuilder::new(&vertices); let builder = if is_adversarial { - let attempts = - NonZeroUsize::new(8).expect("retry attempts must be non-zero"); + let attempts = retry_attempts(8); builder.construction_options( ConstructionOptions::default().with_retry_policy( RetryPolicy::Shuffled { @@ -842,13 +862,13 @@ macro_rules! benchmark_validation_components_dimension { } }) .unwrap_or_else(|| { - panic!( + abort_benchmark(format_args!( "failed to build {}D validation component benchmark triangulation \ after {} seeds (last error: {})", $dim, VALIDATION_SEED_SEARCH_LIMIT, last_error.unwrap_or_else(|| "none".to_string()) - ); + )); }); let mut group = c.benchmark_group(format!("validation_components_{}d{}", $dim, suffix)); @@ -857,29 +877,41 @@ macro_rules! benchmark_validation_components_dimension { group.bench_function("tds_is_valid", |b| { b.iter(|| { - black_box(dt.tds().is_valid()) - .expect("TDS validation should pass for benchmark triangulation"); + if let Err(error) = black_box(dt.tds().is_valid()) { + abort_benchmark(format_args!( + "TDS validation should pass for benchmark triangulation: {error}" + )); + } }); }); group.bench_function("tri_is_valid", |b| { b.iter(|| { - black_box(dt.as_triangulation().is_valid()) - .expect("triangulation validation should pass for benchmark triangulation"); + if let Err(error) = black_box(dt.as_triangulation().is_valid()) { + abort_benchmark(format_args!( + "triangulation validation should pass for benchmark triangulation: {error}" + )); + } }); }); group.bench_function("is_valid_delaunay", |b| { b.iter(|| { - black_box(dt.is_valid()) - .expect("Delaunay validation should pass for benchmark triangulation"); + if let Err(error) = black_box(dt.is_valid()) { + abort_benchmark(format_args!( + "Delaunay validation should pass for benchmark triangulation: {error}" + )); + } }); }); group.bench_function("validate", |b| { b.iter(|| { - black_box(dt.validate()) - .expect("full validation should pass for benchmark triangulation"); + if let Err(error) = black_box(dt.validate()) { + abort_benchmark(format_args!( + "full validation should pass for benchmark triangulation: {error}" + )); + } }); }); @@ -929,8 +961,14 @@ fn bench_bottlenecks(c: &mut Criterion) { }, |dt| { if let Some(dt) = dt { - let boundary_facets = - dt.tds().boundary_facets().expect("boundary_facets failed"); + let boundary_facets = match dt.tds().boundary_facets() { + Ok(value) => value, + Err(error) => { + abort_benchmark(format_args!( + "boundary_facets failed: {error}" + )); + } + }; black_box(boundary_facets); } }, @@ -955,8 +993,12 @@ fn bench_bottlenecks(c: &mut Criterion) { }, |dt| { if let Some(dt) = dt { - let hull = - ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + let hull = match ConvexHull::from_triangulation(dt.as_triangulation()) { + Ok(value) => value, + Err(error) => abort_benchmark(format_args!( + "convex hull extraction failed: {error}" + )), + }; black_box(hull); } }, diff --git a/benches/topology_guarantee_construction.rs b/benches/topology_guarantee_construction.rs index 8ca63b6e..c8bd9a47 100644 --- a/benches/topology_guarantee_construction.rs +++ b/benches/topology_guarantee_construction.rs @@ -21,6 +21,11 @@ use delaunay::vertex; use std::hint::black_box; use std::time::Duration; +/// Shared benchmark setup error helpers. +#[path = "common/bench_utils.rs"] +pub mod bench_utils; +use bench_utils::{abort_benchmark, bench_result}; + const BOUNDS: (f64, f64) = (-100.0, 100.0); const SEED_SALT: u64 = 0x9E37_79B9_7F4A_7C15; @@ -41,8 +46,10 @@ fn bench_dimension( // Deterministic input per (dimension, count). let seed = seed_base ^ (n_points as u64).wrapping_mul(SEED_SALT); - let points = - generate_random_points_seeded::(n_points, BOUNDS, seed).expect("gen points"); + let points = bench_result( + generate_random_points_seeded::(n_points, BOUNDS, seed), + "failed to generate benchmark points", + ); let vertices = points.into_iter().map(|p| vertex!(p)).collect::>(); group.bench_with_input( @@ -63,9 +70,9 @@ fn bench_dimension( for v in vertices { // Use the statistics API so retryable degeneracies can be skipped // (transactional rollback) instead of aborting the benchmark. - let _ = dt - .insert_with_statistics(*v) - .expect("non-retryable insertion error"); + if let Err(error) = dt.insert_with_statistics(*v) { + abort_benchmark(format_args!("non-retryable insertion error: {error}")); + } } // Completion-time PL-manifold certification when required. let _ = dt.as_triangulation().validate_at_completion(); @@ -89,9 +96,9 @@ fn bench_dimension( dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); for v in vertices { - let _ = dt - .insert_with_statistics(*v) - .expect("non-retryable insertion error"); + if let Err(error) = dt.insert_with_statistics(*v) { + abort_benchmark(format_args!("non-retryable insertion error: {error}")); + } } // Completion-time PL-manifold certification when required. @@ -116,9 +123,9 @@ fn bench_dimension( dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never); for v in vertices { - let _ = dt - .insert_with_statistics(*v) - .expect("non-retryable insertion error"); + if let Err(error) = dt.insert_with_statistics(*v) { + abort_benchmark(format_args!("non-retryable insertion error: {error}")); + } } // Completion-time PL-manifold certification when required. let _ = dt.as_triangulation().validate_at_completion(); diff --git a/docs/dev/tooling-alignment.md b/docs/dev/tooling-alignment.md index a15a838d..939eab14 100644 --- a/docs/dev/tooling-alignment.md +++ b/docs/dev/tooling-alignment.md @@ -73,29 +73,31 @@ The following previously deferred checks are now repository-owned Semgrep rules: ## Public Sample Error Handling -Examples, doctests, and benchmarks should model the same error-handling style -the crate asks users to copy: return or route through typed errors instead of -using `.expect(...)` as narrative control flow. +Examples, benchmarks, and public API integration tests should model the same +error-handling style the crate asks users to copy: return or route through +typed errors instead of using `.unwrap()`, `.expect(...)`, or `panic!(...)` as +narrative control flow. -The next enforcement step is to make this a zero-tolerance Semgrep rule for: +This is now enforced by `delaunay.rust.no-public-surface-unwrap-panic` for: - `examples/**/*.rs` - `benches/**/*.rs` -- Rust doc comments in `src/**/*.rs` +- public API integration tests: + - `tests/allocation_api.rs` + - `tests/delaunay_public_api_coverage.rs` + - `tests/prelude_exports.rs` + - `tests/public_*.rs` -Current baseline before that cleanup: - -- `examples/**/*.rs`: 35 `.expect(...)` calls. -- `benches/**/*.rs`: 57 `.expect(...)` calls. -- public Rust doc comments in `src/**/*.rs`: 17 `.expect(...)` calls. - -Keep the baseline synchronized with: +The doctest migration remains intentionally separate because Rust doc comments +still have an existing `.unwrap()`/`.expect(...)` baseline that should be +converted with hidden `Result` wrappers in a focused documentation pass. ```bash just verify-expect-counts ``` -When removing this baseline, prefer: +The `verify-expect-counts` recipe tracks only that remaining doctest baseline. +When extending the zero-tolerance Semgrep rule to doctests, prefer: - `fn main() -> Result<(), ExampleError>` in examples, with local `#[derive(thiserror::Error)]` enums that wrap the crate's typed errors. diff --git a/examples/convex_hull_3d_100_points.rs b/examples/convex_hull_3d_100_points.rs index 76153eac..77345859 100644 --- a/examples/convex_hull_3d_100_points.rs +++ b/examples/convex_hull_3d_100_points.rs @@ -445,8 +445,12 @@ fn performance_analysis(dt: &DelaunayTriangulation, (), (), let len_u32 = u32::try_from(extraction_times.len()).unwrap_or(1u32); let avg_extraction_time: Duration = extraction_times.iter().sum::() / len_u32; - let min_extraction_time = *extraction_times.iter().min().unwrap(); - let max_extraction_time = *extraction_times.iter().max().unwrap(); + let Some(min_extraction_time) = extraction_times.iter().min().copied() else { + return; + }; + let Some(max_extraction_time) = extraction_times.iter().max().copied() else { + return; + }; println!(" Convex Hull Extraction (5 runs):"); println!(" • Average time: {avg_extraction_time:?}"); diff --git a/examples/delaunayize_repair.rs b/examples/delaunayize_repair.rs index cc3894e6..009f4c25 100644 --- a/examples/delaunayize_repair.rs +++ b/examples/delaunayize_repair.rs @@ -20,31 +20,55 @@ use delaunay::prelude::triangulation::delaunayize::*; use delaunay::prelude::triangulation::flips::*; +use delaunay::prelude::triangulation::{ + DelaunayTriangulationConstructionError, DelaunayTriangulationValidationError, +}; // For the generic print_outcome helper. use delaunay::prelude::DataType; use delaunay::prelude::geometry::CoordinateScalar; -fn main() { +#[derive(Debug, thiserror::Error)] +enum DelaunayizeRepairExampleError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Delaunayize(#[from] DelaunayizeError), + #[error(transparent)] + Validation(#[from] DelaunayTriangulationValidationError), + #[error(transparent)] + Flip(#[from] FlipError), +} + +#[expect( + clippy::result_large_err, + reason = "example preserves the crate's typed repair errors instead of erasing them" +)] +fn main() -> Result<(), DelaunayizeRepairExampleError> { println!("============================================================"); println!("Delaunayize-by-Flips Repair Workflow"); println!("============================================================\n"); - already_delaunay_3d(); + already_delaunay_3d()?; println!("\n------------------------------------------------------------\n"); - already_delaunay_4d(); + already_delaunay_4d()?; println!("\n------------------------------------------------------------\n"); - flip_then_repair_2d(); + flip_then_repair_2d()?; println!("\n------------------------------------------------------------\n"); - custom_config_2d(); + custom_config_2d()?; println!("\n============================================================"); println!("Example completed successfully!"); println!("============================================================"); + Ok(()) } /// A 3D triangulation that is already Delaunay — delaunayize is a no-op. -fn already_delaunay_3d() { +#[expect( + clippy::result_large_err, + reason = "example preserves the crate's typed repair errors instead of erasing them" +)] +fn already_delaunay_3d() -> Result<(), DelaunayizeRepairExampleError> { println!("1. Already-Delaunay 3D triangulation (no-op)"); println!("--------------------------------------------\n"); @@ -55,8 +79,7 @@ fn already_delaunay_3d() { vertex!([0.0, 0.0, 1.0]), vertex!([0.5, 0.5, 0.5]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); + let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; println!( " Built 3D triangulation: {} vertices, {} cells", @@ -64,15 +87,20 @@ fn already_delaunay_3d() { dt.number_of_cells() ); - let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap(); + let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default())?; print_outcome(&outcome); - dt.validate().unwrap(); + dt.validate()?; println!(" ✓ Full validation (Levels 1–4) passed"); + Ok(()) } /// A 4D triangulation — shows the workflow is dimension-generic. -fn already_delaunay_4d() { +#[expect( + clippy::result_large_err, + reason = "example preserves the crate's typed repair errors instead of erasing them" +)] +fn already_delaunay_4d() -> Result<(), DelaunayizeRepairExampleError> { println!("2. Already-Delaunay 4D triangulation (no-op)"); println!("--------------------------------------------\n"); @@ -84,8 +112,7 @@ fn already_delaunay_4d() { vertex!([0.0, 0.0, 0.0, 1.0]), vertex!([0.25, 0.25, 0.25, 0.25]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 4> = - DelaunayTriangulation::new(&vertices).unwrap(); + let mut dt: DelaunayTriangulation<_, (), (), 4> = DelaunayTriangulation::new(&vertices)?; println!( " Built 4D triangulation: {} vertices, {} cells", @@ -93,17 +120,22 @@ fn already_delaunay_4d() { dt.number_of_cells() ); - let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap(); + let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default())?; print_outcome(&outcome); - dt.validate().unwrap(); + dt.validate()?; println!(" ✓ Full validation (Levels 1–4) passed"); + Ok(()) } /// Apply a k=2 flip in 2D to break the Delaunay property, then repair. /// /// 2D with 7 points guarantees interior facets that are flippable. -fn flip_then_repair_2d() { +#[expect( + clippy::result_large_err, + reason = "example preserves the crate's typed repair errors instead of erasing them" +)] +fn flip_then_repair_2d() -> Result<(), DelaunayizeRepairExampleError> { println!("3. Flip breaks Delaunay in 2D → delaunayize restores it"); println!("-------------------------------------------------------\n"); @@ -116,8 +148,7 @@ fn flip_then_repair_2d() { vertex!([1.0, 1.0]), vertex!([3.0, 1.0]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); + let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices)?; println!( " Initial: {} vertices, {} cells", @@ -150,16 +181,16 @@ fn flip_then_repair_2d() { let Some(facet) = violating_facet else { println!(" (No k=2 flip produced a non-Delaunay state — skipping repair demonstration)"); - return; + return Ok(()); }; - dt.flip_k2(facet).unwrap(); + dt.flip_k2(facet)?; match dt.is_valid() { Ok(()) => { println!( " Applied selected k=2 flip, but Delaunay property remained satisfied (unexpected)" ); - return; + return Ok(()); } Err(err) => { println!(" Applied k=2 flip; post-flip check confirms Delaunay violation: {err}"); @@ -167,15 +198,20 @@ fn flip_then_repair_2d() { } // Repair. - let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap(); + let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default())?; print_outcome(&outcome); - dt.validate().unwrap(); + dt.validate()?; println!(" ✓ Delaunay property restored"); + Ok(()) } /// Custom configuration with tight budgets and fallback enabled. -fn custom_config_2d() { +#[expect( + clippy::result_large_err, + reason = "example preserves the crate's typed repair errors instead of erasing them" +)] +fn custom_config_2d() -> Result<(), DelaunayizeRepairExampleError> { println!("4. Custom configuration (2D, fallback enabled)"); println!("----------------------------------------------\n"); @@ -186,8 +222,7 @@ fn custom_config_2d() { vertex!([1.0, 1.0]), vertex!([0.5, 0.5]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).unwrap(); + let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices)?; let config = DelaunayizeConfig { topology_max_iterations: 10, @@ -201,15 +236,16 @@ fn custom_config_2d() { config.topology_max_iterations, config.topology_max_cells_removed, config.fallback_rebuild, ); - let outcome = delaunayize_by_flips(&mut dt, config).unwrap(); + let outcome = delaunayize_by_flips(&mut dt, config)?; print_outcome(&outcome); - dt.validate().unwrap(); + dt.validate()?; println!( " ✓ Valid 2D triangulation: {} vertices, {} cells", dt.number_of_vertices(), dt.number_of_cells(), ); + Ok(()) } fn print_outcome( diff --git a/examples/diagnostics.rs b/examples/diagnostics.rs index 20f699cc..38d40720 100644 --- a/examples/diagnostics.rs +++ b/examples/diagnostics.rs @@ -8,6 +8,8 @@ //! cargo run --features diagnostics --example diagnostics //! ``` +#[cfg(feature = "diagnostics")] +use delaunay::prelude::DelaunayValidationError; #[cfg(feature = "diagnostics")] use delaunay::prelude::diagnostics::{ debug_print_first_delaunay_violation, delaunay_violation_report, @@ -16,23 +18,36 @@ use delaunay::prelude::diagnostics::{ use delaunay::prelude::geometry::AdaptiveKernel; #[cfg(feature = "diagnostics")] use delaunay::prelude::triangulation::{ - DelaunayTriangulation, DelaunayTriangulationValidationError, flips::*, + DelaunayTriangulation, DelaunayTriangulationConstructionError, + DelaunayTriangulationValidationError, flips::*, }; #[cfg(feature = "diagnostics")] use delaunay::vertex; #[cfg(feature = "diagnostics")] -fn main() { +#[derive(Debug, thiserror::Error)] +enum DiagnosticsExampleError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + DelaunayValidation(#[from] DelaunayValidationError), + #[error("expected at least one public k=2 flip to produce a Delaunay violation")] + NoDelaunayViolatingFlip, +} + +#[cfg(feature = "diagnostics")] +fn main() -> Result<(), DiagnosticsExampleError> { init_tracing(); println!("Diagnostics feature example"); println!("===========================\n"); - report_valid_triangulation(); + report_valid_triangulation()?; println!(); - report_non_delaunay_triangulation(); + report_non_delaunay_triangulation()?; println!("\nDone. Set RUST_LOG=delaunay=debug to see verbose tracing output."); + Ok(()) } #[cfg(not(feature = "diagnostics"))] @@ -51,16 +66,16 @@ fn init_tracing() { /// Shows the shape of an empty diagnostics report for a valid triangulation. #[cfg(feature = "diagnostics")] -fn report_valid_triangulation() { +fn report_valid_triangulation() -> Result<(), DiagnosticsExampleError> { let vertices = vec![ vertex!([0.0, 0.0, 0.0]), vertex!([1.0, 0.0, 0.0]), vertex!([0.0, 1.0, 0.0]), vertex!([0.0, 0.0, 1.0]), ]; - let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; - let report = delaunay_violation_report(dt.tds(), None).unwrap(); + let report = delaunay_violation_report(dt.tds(), None)?; println!("Valid 3D triangulation:"); println!(" vertices: {}", report.number_of_vertices); @@ -68,13 +83,14 @@ fn report_valid_triangulation() { println!(" checked cells: {}", report.checked_cells); println!(" Delaunay valid: {}", report.is_valid()); assert!(report.is_valid()); + Ok(()) } /// Applies a public topology edit that breaks Delaunayness, then reports the violation. #[cfg(feature = "diagnostics")] -fn report_non_delaunay_triangulation() { - let dt = build_non_delaunay_triangulation_2d(); - let report = delaunay_violation_report(dt.tds(), None).unwrap(); +fn report_non_delaunay_triangulation() -> Result<(), DiagnosticsExampleError> { + let dt = build_non_delaunay_triangulation_2d()?; + let report = delaunay_violation_report(dt.tds(), None)?; println!("Non-Delaunay 2D triangulation after an explicit k=2 flip:"); println!(" vertices: {}", report.number_of_vertices); @@ -89,11 +105,13 @@ fn report_non_delaunay_triangulation() { } debug_print_first_delaunay_violation(dt.tds(), None); + Ok(()) } /// Builds a valid 2D triangulation and returns a clone after a k=2 flip creates a violation. #[cfg(feature = "diagnostics")] -fn build_non_delaunay_triangulation_2d() -> DelaunayTriangulation, (), (), 2> { +fn build_non_delaunay_triangulation_2d() +-> Result, (), (), 2>, DiagnosticsExampleError> { let vertices = vec![ vertex!([0.0, 0.0]), vertex!([4.0, 0.0]), @@ -103,7 +121,7 @@ fn build_non_delaunay_triangulation_2d() -> DelaunayTriangulation = DelaunayTriangulation::new(&vertices).unwrap(); + let dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices)?; for (cell_key, cell) in dt.cells() { if let Some(neighbors) = cell.neighbors() { @@ -124,11 +142,11 @@ fn build_non_delaunay_triangulation_2d() -> DelaunayTriangulation Result<(), NumericalRobustnessExampleError> { println!("Numerical robustness example"); println!("============================\n"); - compare_orientation_kernels(); + compare_orientation_kernels()?; println!(); - compare_insphere_boundary_handling(); + compare_insphere_boundary_handling()?; println!(); - build_with_adaptive_kernel(); + build_with_adaptive_kernel()?; + Ok(()) } /// Compares orientation predicate behavior on a degenerate collinear simplex. -fn compare_orientation_kernels() { +fn compare_orientation_kernels() -> Result<(), NumericalRobustnessExampleError> { let collinear = [ Point::new([0.0, 0.0]), Point::new([1.0, 1.0]), @@ -35,10 +51,10 @@ fn compare_orientation_kernels() { let robust = RobustKernel::::new(); let adaptive = AdaptiveKernel::::new(); - let direct_robust = robust_orientation(&collinear).unwrap(); - let fast_sign = fast.orientation(&collinear).unwrap(); - let robust_sign = robust.orientation(&collinear).unwrap(); - let adaptive_sign = adaptive.orientation(&collinear).unwrap(); + let direct_robust = robust_orientation(&collinear)?; + let fast_sign = fast.orientation(&collinear)?; + let robust_sign = robust.orientation(&collinear)?; + let adaptive_sign = adaptive.orientation(&collinear)?; println!("Collinear orientation:"); println!(" robust_orientation: {direct_robust:?}"); @@ -48,10 +64,11 @@ fn compare_orientation_kernels() { assert_eq!(robust_sign, 0); assert_ne!(adaptive_sign, 0); + Ok(()) } /// Compares explicit boundary reporting with adaptive `SoS` tie-breaking. -fn compare_insphere_boundary_handling() { +fn compare_insphere_boundary_handling() -> Result<(), NumericalRobustnessExampleError> { let simplex = [ Point::new([0.0, 0.0]), Point::new([1.0, 0.0]), @@ -62,9 +79,9 @@ fn compare_insphere_boundary_handling() { let robust = RobustKernel::::new(); let adaptive = AdaptiveKernel::::new(); - let direct_robust = robust_insphere(&simplex, &boundary_point).unwrap(); - let robust_sign = robust.in_sphere(&simplex, &boundary_point).unwrap(); - let adaptive_sign = adaptive.in_sphere(&simplex, &boundary_point).unwrap(); + let direct_robust = robust_insphere(&simplex, &boundary_point)?; + let robust_sign = robust.in_sphere(&simplex, &boundary_point)?; + let adaptive_sign = adaptive.in_sphere(&simplex, &boundary_point)?; println!("Cospherical insphere query:"); println!(" robust_insphere: {direct_robust:?}"); @@ -73,10 +90,11 @@ fn compare_insphere_boundary_handling() { assert_eq!(robust_sign, 0); assert_ne!(adaptive_sign, 0); + Ok(()) } /// Builds a small triangulation with the default exact adaptive kernel and validates it. -fn build_with_adaptive_kernel() { +fn build_with_adaptive_kernel() -> Result<(), NumericalRobustnessExampleError> { let vertices = vec![ vertex!([0.0, 0.0, 0.0]), vertex!([1.0, 0.0, 0.0]), @@ -86,12 +104,13 @@ fn build_with_adaptive_kernel() { ]; let dt: DelaunayTriangulation, (), (), 3> = - DelaunayTriangulation::new(&vertices).unwrap(); + DelaunayTriangulation::new(&vertices)?; - dt.validate().unwrap(); + dt.validate()?; println!( "Adaptive construction: {} vertices, {} cells, full validation passed", dt.number_of_vertices(), dt.number_of_cells() ); + Ok(()) } diff --git a/examples/pachner_roundtrip_4d.rs b/examples/pachner_roundtrip_4d.rs index 2ebd9985..0becc8eb 100644 --- a/examples/pachner_roundtrip_4d.rs +++ b/examples/pachner_roundtrip_4d.rs @@ -71,6 +71,8 @@ enum PachnerRoundtripError { Validation(#[from] DelaunayTriangulationValidationError), #[error(transparent)] Flip(#[from] FlipError), + #[error(transparent)] + IntegerConversion(#[from] std::num::TryFromIntError), #[error("triangulation has no cells")] EmptyTriangulation, #[error("cell {cell_key:?} not found in TDS")] @@ -238,8 +240,7 @@ fn cell_centroid(dt: &Dt4, cell_key: CellKey) -> Result<[f64; 4], PachnerRoundtr } } - let vertex_count = - u32::try_from(cell.vertices().len()).expect("cell vertex count should fit in u32"); + let vertex_count = u32::try_from(cell.vertices().len())?; let inv = 1.0_f64 / f64::from(vertex_count); for coord in &mut coords { *coord *= inv; @@ -275,7 +276,9 @@ fn collect_interior_facets(dt: &Dt4) -> Vec { if let Some(neighbors) = cell.neighbors() { for (facet_index, neighbor) in neighbors.iter().enumerate() { if neighbor.is_some() { - let facet_index = u8::try_from(facet_index).expect("facet index fits in u8"); + let Ok(facet_index) = u8::try_from(facet_index) else { + continue; + }; facets.push(FacetHandle::new(cell_key, facet_index)); } } @@ -324,8 +327,12 @@ fn collect_ridges(dt: &Dt4) -> Vec { let vertex_count = cell.number_of_vertices(); for i in 0..vertex_count { for j in (i + 1)..vertex_count { - let omit_a = u8::try_from(i).expect("ridge index fits in u8"); - let omit_b = u8::try_from(j).expect("ridge index fits in u8"); + let Ok(omit_a) = u8::try_from(i) else { + continue; + }; + let Ok(omit_b) = u8::try_from(j) else { + continue; + }; ridges.push(RidgeHandle::new(cell_key, omit_a, omit_b)); } } diff --git a/examples/topology_editing_2d_3d.rs b/examples/topology_editing_2d_3d.rs index 081696f2..9db04a6b 100644 --- a/examples/topology_editing_2d_3d.rs +++ b/examples/topology_editing_2d_3d.rs @@ -18,45 +18,84 @@ //! cargo run --example topology_editing_2d_3d //! ``` -use delaunay::prelude::geometry::{Coordinate, Kernel, Point, circumcenter, hypot}; +#![expect( + clippy::result_large_err, + reason = "example preserves the crate's typed insertion and flip errors instead of erasing them" +)] + +use delaunay::prelude::geometry::{ + CircumcenterError, Coordinate, Kernel, Point, circumcenter, hypot, +}; use delaunay::prelude::triangulation::flips::*; use delaunay::prelude::triangulation::*; +use delaunay::prelude::{TdsError, VertexKey}; + +type ExampleResult = Result; + +#[derive(Debug, thiserror::Error)] +enum TopologyEditingExampleError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Validation(#[from] DelaunayTriangulationValidationError), + #[error(transparent)] + Insertion(#[from] InsertionError), + #[error(transparent)] + Flip(#[from] FlipError), + #[error(transparent)] + Tds(#[from] TdsError), + #[error(transparent)] + Circumcenter(#[from] CircumcenterError), + #[error("{demo} triangulation has no cells")] + EmptyTriangulation { demo: &'static str }, + #[error("{demo} cell key was not found")] + MissingCell { demo: &'static str }, + #[error("{demo} vertex key {vertex_key:?} was not found")] + MissingVertex { + demo: &'static str, + vertex_key: VertexKey, + }, + #[error("{demo} has no interior facet")] + NoInteriorFacet { demo: &'static str }, +} -fn main() { +fn main() -> ExampleResult { println!("============================================================"); println!("Topology Editing: Builder API vs Edit API (2D and 3D)"); println!("============================================================\n"); // Part 1: 2D examples (k=1 and k=2 flips) - demo_2d(); + demo_2d()?; println!("\n############################################################\n"); // Part 2: 3D examples (k=1, k=2, and k=3 flips) - demo_3d(); + demo_3d()?; println!("\n============================================================"); println!("Example complete!"); println!("============================================================"); + Ok(()) } // ============================================================================ // 2D DEMONSTRATIONS // ============================================================================ -fn demo_2d() { +fn demo_2d() -> ExampleResult { println!("PART 1: 2D TRIANGULATION"); println!("============================================================\n"); - builder_api_2d(); + builder_api_2d()?; println!("\n------------------------------------------------------------\n"); - edit_api_2d_k1(); + edit_api_2d_k1()?; println!("\n------------------------------------------------------------\n"); - edit_api_2d_k2(); + edit_api_2d_k2()?; + Ok(()) } /// Demonstrates the Builder API in 2D. -fn builder_api_2d() { +fn builder_api_2d() -> ExampleResult { println!("2D Builder API: Automatic Delaunay Preservation"); println!("------------------------------------------------\n"); @@ -67,13 +106,11 @@ fn builder_api_2d() { vertex!([2.0, 3.0]), ]; - let mut dt = DelaunayTriangulationBuilder::new(&vertices) - .build::<()>() - .expect("Failed to construct triangulation"); + let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; println!("Initial triangle:"); print_stats_2d(&dt); - dt.validate().expect("Should be valid"); + dt.validate()?; println!(" ✓ Delaunay property verified\n"); // Insert vertices using Builder API @@ -85,7 +122,7 @@ fn builder_api_2d() { ]; for (i, v) in new_vertices.into_iter().enumerate() { - dt.insert(v).expect("Insertion should succeed"); + dt.insert(v)?; println!( " After insert {}: {} vertices, {} cells", i + 1, @@ -95,12 +132,13 @@ fn builder_api_2d() { } // Verify Delaunay property is maintained - dt.validate().expect("Should remain valid"); + dt.validate()?; println!("\n✓ Builder API automatically maintained Delaunay property"); + Ok(()) } /// Demonstrates k=1 flips (cell split/merge) in 2D. -fn edit_api_2d_k1() { +fn edit_api_2d_k1() -> ExampleResult { println!("2D Edit API: k=1 Flips (Cell Split/Merge)"); println!("------------------------------------------\n"); @@ -110,22 +148,36 @@ fn edit_api_2d_k1() { vertex!([1.5, 2.5]), ]; - let mut dt = DelaunayTriangulationBuilder::new(&vertices) - .build::<()>() - .expect("Failed to construct triangulation"); + let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; println!("Initial triangle:"); print_stats_2d(&dt); // Apply k=1 flip (insert vertex into cell) - let cell_key = dt.cells().next().unwrap().0; - let cell = dt.tds().cell(cell_key).expect("Cell should exist"); + let cell_key = dt.cells().next().map(|(cell_key, _)| cell_key).ok_or( + TopologyEditingExampleError::EmptyTriangulation { + demo: "2D k=1 demo", + }, + )?; + let cell = dt + .tds() + .cell(cell_key) + .ok_or(TopologyEditingExampleError::MissingCell { + demo: "2D k=1 demo", + })?; let vertex_points: Vec> = cell .vertices() .iter() - .map(|vkey| *dt.tds().vertex(*vkey).expect("Vertex should exist").point()) - .collect(); - let circumcenter = circumcenter(&vertex_points).expect("Circumcenter should exist"); + .map(|vkey| { + dt.tds().vertex(*vkey).map(|vertex| *vertex.point()).ok_or( + TopologyEditingExampleError::MissingVertex { + demo: "2D k=1 demo", + vertex_key: *vkey, + }, + ) + }) + .collect::>()?; + let circumcenter = circumcenter(&vertex_points)?; let circumcenter_coords = circumcenter.to_array(); let distances: Vec = vertex_points .iter() @@ -145,9 +197,7 @@ fn edit_api_2d_k1() { circumcenter_coords[0], circumcenter_coords[1] ); - let flip_info = dt - .flip_k1_insert(cell_key, vertex!(circumcenter_coords)) - .expect("k=1 flip should succeed"); + let flip_info = dt.flip_k1_insert(cell_key, vertex!(circumcenter_coords))?; println!("After k=1 forward:"); print_stats_2d(&dt); @@ -156,25 +206,25 @@ fn edit_api_2d_k1() { println!(" New vertex: {:?}", flip_info.inserted_face_vertices); // Verify structural validity (always maintained) - dt.tds().is_valid().expect("Structure should be valid"); + dt.tds().is_valid()?; println!(" ✓ Structural invariants preserved"); // Apply inverse k=1 flip (remove vertex) println!("\nApplying k=1 inverse (remove vertex):"); let vertex_to_remove = flip_info.inserted_face_vertices[0]; - dt.flip_k1_remove(vertex_to_remove) - .expect("k=1 inverse should succeed"); + dt.flip_k1_remove(vertex_to_remove)?; println!("After k=1 inverse:"); print_stats_2d(&dt); // Verify we're back to original state - dt.validate().expect("Should be valid"); + dt.validate()?; println!("\n✓ k=1 flip roundtrip successful (Edit API)"); + Ok(()) } /// Demonstrates k=2 flips (edge flip) in 2D. -fn edit_api_2d_k2() { +fn edit_api_2d_k2() -> ExampleResult { println!("2D Edit API: k=2 Flips (Edge Flip)"); println!("-----------------------------------\n"); @@ -186,9 +236,7 @@ fn edit_api_2d_k2() { vertex!([0.0, 2.0]), ]; - let mut dt = DelaunayTriangulationBuilder::new(&vertices) - .build::<()>() - .expect("Failed to construct triangulation"); + let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; println!("Initial square (2 triangles):"); print_stats_2d(&dt); @@ -200,10 +248,13 @@ fn edit_api_2d_k2() { ); // Find an interior edge to flip - let facet = find_interior_facet_2d(&dt).expect("Should have an interior edge"); + let facet = + find_interior_facet_2d(&dt).ok_or(TopologyEditingExampleError::NoInteriorFacet { + demo: "2D k=2 demo", + })?; println!("\nApplying k=2 flip (flipping diagonal edge):"); - let flip_info = dt.flip_k2(facet).expect("k=2 flip should succeed"); + let flip_info = dt.flip_k2(facet)?; println!("After k=2 forward:"); print_stats_2d(&dt); @@ -226,27 +277,29 @@ fn edit_api_2d_k2() { println!(" In 2D, k=2 flips are always reversible by another k=2 flip"); println!("\n✓ k=2 flip successful (Edit API)"); + Ok(()) } // ============================================================================ // 3D DEMONSTRATIONS // ============================================================================ -fn demo_3d() { +fn demo_3d() -> ExampleResult { println!("PART 2: 3D TRIANGULATION"); println!("============================================================\n"); - builder_api_3d(); + builder_api_3d()?; println!("\n------------------------------------------------------------\n"); - edit_api_3d_k1(); + edit_api_3d_k1()?; println!("\n------------------------------------------------------------\n"); - edit_api_3d_k2(); + edit_api_3d_k2()?; println!("\n------------------------------------------------------------\n"); - edit_api_3d_k3(); + edit_api_3d_k3()?; + Ok(()) } /// Demonstrates the Builder API in 3D. -fn builder_api_3d() { +fn builder_api_3d() -> ExampleResult { println!("3D Builder API: Automatic Delaunay Preservation"); println!("------------------------------------------------\n"); @@ -257,13 +310,11 @@ fn builder_api_3d() { vertex!([1.0, 0.5, 1.5]), ]; - let mut dt = DelaunayTriangulationBuilder::new(&vertices) - .build::<()>() - .expect("Failed to construct triangulation"); + let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; println!("Initial tetrahedron:"); print_stats_3d(&dt); - dt.validate().expect("Should be valid"); + dt.validate()?; println!(" ✓ Delaunay property verified\n"); // Insert vertices using Builder API @@ -271,7 +322,7 @@ fn builder_api_3d() { let new_vertices = vec![vertex!([1.0, 0.5, 0.5]), vertex!([0.8, 0.8, 0.8])]; for (i, v) in new_vertices.into_iter().enumerate() { - dt.insert(v).expect("Insertion should succeed"); + dt.insert(v)?; println!( " After insert {}: {} vertices, {} cells", i + 1, @@ -280,12 +331,13 @@ fn builder_api_3d() { ); } - dt.validate().expect("Should remain valid"); + dt.validate()?; println!("\n✓ Builder API automatically maintained Delaunay property"); + Ok(()) } /// Demonstrates k=1 flips in 3D. -fn edit_api_3d_k1() { +fn edit_api_3d_k1() -> ExampleResult { println!("3D Edit API: k=1 Flips (Cell Split/Merge)"); println!("------------------------------------------\n"); @@ -296,22 +348,36 @@ fn edit_api_3d_k1() { vertex!([1.0, 0.5, 1.5]), ]; - let mut dt = DelaunayTriangulationBuilder::new(&vertices) - .build::<()>() - .expect("Failed to construct triangulation"); + let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; println!("Initial tetrahedron:"); print_stats_3d(&dt); // Apply k=1 flip - let cell_key = dt.cells().next().unwrap().0; - let cell = dt.tds().cell(cell_key).expect("Cell should exist"); + let cell_key = dt.cells().next().map(|(cell_key, _)| cell_key).ok_or( + TopologyEditingExampleError::EmptyTriangulation { + demo: "3D k=1 demo", + }, + )?; + let cell = dt + .tds() + .cell(cell_key) + .ok_or(TopologyEditingExampleError::MissingCell { + demo: "3D k=1 demo", + })?; let vertex_points: Vec> = cell .vertices() .iter() - .map(|vkey| *dt.tds().vertex(*vkey).expect("Vertex should exist").point()) - .collect(); - let circumcenter = circumcenter(&vertex_points).expect("Circumcenter should exist"); + .map(|vkey| { + dt.tds().vertex(*vkey).map(|vertex| *vertex.point()).ok_or( + TopologyEditingExampleError::MissingVertex { + demo: "3D k=1 demo", + vertex_key: *vkey, + }, + ) + }) + .collect::>()?; + let circumcenter = circumcenter(&vertex_points)?; let circumcenter_coords = circumcenter.to_array(); let distances: Vec = vertex_points .iter() @@ -331,9 +397,7 @@ fn edit_api_3d_k1() { "\nApplying k=1 flip (split tetrahedron at circumcenter [{:.2}, {:.2}, {:.2}]):", circumcenter_coords[0], circumcenter_coords[1], circumcenter_coords[2] ); - let flip_info = dt - .flip_k1_insert(cell_key, vertex!(circumcenter_coords)) - .expect("k=1 flip should succeed"); + let flip_info = dt.flip_k1_insert(cell_key, vertex!(circumcenter_coords))?; println!("After k=1 forward:"); print_stats_3d(&dt); @@ -342,17 +406,17 @@ fn edit_api_3d_k1() { // Apply inverse println!("\nApplying k=1 inverse:"); let vertex_to_remove = flip_info.inserted_face_vertices[0]; - dt.flip_k1_remove(vertex_to_remove) - .expect("k=1 inverse should succeed"); + dt.flip_k1_remove(vertex_to_remove)?; println!("After k=1 inverse:"); print_stats_3d(&dt); println!("\n✓ k=1 flip roundtrip successful in 3D"); + Ok(()) } /// Demonstrates k=2 flips (facet flip) in 3D. -fn edit_api_3d_k2() { +fn edit_api_3d_k2() -> ExampleResult { println!("3D Edit API: k=2 Flips (Facet Flip: 2↔3)"); println!("-----------------------------------------\n"); @@ -368,9 +432,7 @@ fn edit_api_3d_k2() { vertex!([1.0, 0.6, 0.4]), // Interior point ]; - let mut dt = DelaunayTriangulationBuilder::new(&vertices) - .build::<()>() - .expect("Failed to construct triangulation"); + let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; println!("Triangulation with interior point:"); print_stats_3d(&dt); @@ -413,10 +475,12 @@ fn edit_api_3d_k2() { } else { println!("⚠️ No interior facet found for k=2 flip demo"); } + + Ok(()) } /// Demonstrates k=3 flips (ridge flip) in 3D. -fn edit_api_3d_k3() { +fn edit_api_3d_k3() -> ExampleResult { println!("3D Edit API: k=3 Flips (Ridge Flip: 3↔2)"); println!("-----------------------------------------\n"); println!("Note: k=3 flips are only available in 3D and higher dimensions\n"); @@ -441,32 +505,29 @@ fn edit_api_3d_k3() { vertex!([1.0, 0.6, 1.4]), ]; - if let Ok(mut dt) = DelaunayTriangulationBuilder::new(&vertices).build::<()>() { - print_stats_3d(&dt); - - // Try to find and flip a ridge - if let Some(ridge) = find_flippable_ridge_3d(&dt) { - match dt.flip_k3(ridge) { - Ok(flip_info) => { - println!("\n✓ k=3 flip succeeded:"); - print_stats_3d(&dt); - println!(" Removed: {} cells", flip_info.removed_cells.len()); - println!(" Inserted: {} cells", flip_info.new_cells.len()); - } - Err(e) => { - println!("\n⚠️ k=3 flip not applicable: {e}"); - println!(" (Geometric constraints not satisfied for this configuration)"); - } + let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; + print_stats_3d(&dt); + + // Try to find and flip a ridge + if let Some(ridge) = find_flippable_ridge_3d(&dt) { + match dt.flip_k3(ridge) { + Ok(flip_info) => { + println!("\n✓ k=3 flip succeeded:"); + print_stats_3d(&dt); + println!(" Removed: {} cells", flip_info.removed_cells.len()); + println!(" Inserted: {} cells", flip_info.new_cells.len()); + } + Err(e) => { + println!("\n⚠️ k=3 flip not applicable: {e}"); + println!(" (Geometric constraints not satisfied for this configuration)"); } - } else { - println!("\n⚠️ No ridges found in this simple triangulation"); } } else { - println!("\n⚠️ Note: k=3 flips require complex geometric configurations"); - println!(" This example demonstrates that the API is available in 3D+"); + println!("\n⚠️ No ridges found in this simple triangulation"); } println!("\n✓ k=3 flip API demonstrated (Edit API - 3D+ only)"); + Ok(()) } // ============================================================================ @@ -496,7 +557,9 @@ fn find_interior_facet_2d>( if let Some(neighbors) = cell.neighbors() { for (facet_idx, neighbor) in neighbors.iter().enumerate() { if neighbor.is_some() { - let facet_idx = u8::try_from(facet_idx).expect("facet index fits in u8"); + let Ok(facet_idx) = u8::try_from(facet_idx) else { + continue; + }; return Some(FacetHandle::new(cell_key, facet_idx)); } } @@ -512,7 +575,9 @@ fn find_interior_facet_3d>( if let Some(neighbors) = cell.neighbors() { for (facet_idx, neighbor) in neighbors.iter().enumerate() { if neighbor.is_some() { - let facet_idx = u8::try_from(facet_idx).expect("facet index fits in u8"); + let Ok(facet_idx) = u8::try_from(facet_idx) else { + continue; + }; return Some(FacetHandle::new(cell_key, facet_idx)); } } @@ -532,8 +597,12 @@ fn find_flippable_ridge_3d>( if i + 1 >= vertex_count { continue; } - let omit_a = u8::try_from(i).expect("ridge index fits in u8"); - let omit_b = u8::try_from(i + 1).expect("ridge index fits in u8"); + let Ok(omit_a) = u8::try_from(i) else { + continue; + }; + let Ok(omit_b) = u8::try_from(i + 1) else { + continue; + }; let ridge = RidgeHandle::new(cell_key, omit_a, omit_b); // Just return the first one we find diff --git a/examples/triangulation_3d_100_points.rs b/examples/triangulation_3d_100_points.rs index a5b79a9f..70edac09 100644 --- a/examples/triangulation_3d_100_points.rs +++ b/examples/triangulation_3d_100_points.rs @@ -25,16 +25,37 @@ //! - Boundary analysis //! - Performance metrics +use delaunay::prelude::AdjacencyIndexBuildError; use delaunay::prelude::generators::generate_random_triangulation; use delaunay::prelude::query::*; use delaunay::prelude::topology::validation as topology_validation; +use delaunay::prelude::triangulation::DelaunayTriangulationConstructionError; use num_traits::NumCast; use num_traits::cast::cast; -use std::time::Instant; +use std::{ + env, + time::{Duration, Instant}, +}; const SEED_CANDIDATES: &[u64] = &[1, 7, 11, 42, 99, 123, 666]; -fn main() { +#[derive(Debug, thiserror::Error)] +enum Triangulation3dExampleError { + #[error(transparent)] + AdjacencyIndex(#[from] AdjacencyIndexBuildError), + #[error("failed to create triangulation after trying seeds {seed_candidates:?}: {source}")] + TriangulationConstruction { + seed_candidates: Vec, + #[source] + source: DelaunayTriangulationConstructionError, + }, + #[error( + "failed to create triangulation because no seed candidates were available: {seed_candidates:?}" + )] + MissingSeedCandidates { seed_candidates: Vec }, +} + +fn main() -> Result<(), Triangulation3dExampleError> { println!("================================================================="); println!("3D Delaunay Triangulation Example - 100 Random Points"); println!("=================================================================\\n"); @@ -43,10 +64,9 @@ fn main() { // Use a fixed seed + bounds so that `just examples` is reproducible and robust. let n_points = 100; let bounds = (-3.0, 3.0); - let seed_override: Option = std::env::var("DELAUNAY_EXAMPLE_SEED") + let seed_override: Option = env::var("DELAUNAY_EXAMPLE_SEED") .ok() - .and_then(|value| value.parse().ok()) - .or(None); + .and_then(|value| value.parse().ok()); let seed_candidates: Vec = seed_override.map_or_else(|| SEED_CANDIDATES.to_vec(), |seed| vec![seed]); @@ -56,7 +76,7 @@ fn main() { ); let start = Instant::now(); - let mut last_error: Option = None; + let mut last_error: Option = None; let mut used_seed: Option = None; let mut dt: Option, (), (), 3>> = None; for &candidate in &seed_candidates { @@ -66,28 +86,27 @@ fn main() { used_seed = Some(candidate); break; } - Err(e) => { - last_error = Some(format!("{e}")); + Err(error) => { + last_error = Some(error); } } } - let dt: DelaunayTriangulation, (), (), 3> = if let Some(triangulation) = dt - { - let construction_time = start.elapsed(); - if let Some(seed) = used_seed { - println!("✓ Triangulation created successfully in {construction_time:?} (seed={seed})"); - } else { - println!("✓ Triangulation created successfully in {construction_time:?}"); - } - triangulation - } else { - eprintln!( - "✗ Failed to create triangulation after trying seeds {seed_candidates:?}: {}", - last_error.unwrap_or_else(|| "unknown error".to_string()) - ); - return; + let Some(dt) = dt else { + let Some(source) = last_error else { + return Err(Triangulation3dExampleError::MissingSeedCandidates { seed_candidates }); + }; + return Err(Triangulation3dExampleError::TriangulationConstruction { + seed_candidates, + source, + }); }; + let construction_time = start.elapsed(); + if let Some(seed) = used_seed { + println!("✓ Triangulation created successfully in {construction_time:?} (seed={seed})"); + } else { + println!("✓ Triangulation created successfully in {construction_time:?}"); + } // Display some vertex information by accessing the triangulation's vertices let vertex_count = dt.tds().number_of_vertices(); @@ -107,7 +126,7 @@ fn main() { println!(); // Display triangulation properties - analyze_triangulation(&dt); + analyze_triangulation(&dt)?; // Validate the triangulation validate_triangulation(&dt); @@ -121,10 +140,13 @@ fn main() { println!("\n================================================================="); println!("Example completed successfully!"); println!("================================================================="); + Ok(()) } /// Analyze and display triangulation properties -fn analyze_triangulation(dt: &DelaunayTriangulation) +fn analyze_triangulation( + dt: &DelaunayTriangulation, +) -> Result<(), Triangulation3dExampleError> where K: Kernel, K::Scalar: NumCast, @@ -140,9 +162,7 @@ where // Demonstrate the public topology traversal API using an opt-in adjacency index. // This avoids per-call allocations in methods like edges()/incident_edges(). let tri = dt.as_triangulation(); - let index = tri - .build_adjacency_index() - .expect("adjacency index should build for a valid triangulation"); + let index = tri.build_adjacency_index()?; let edge_count = tri.number_of_edges_with_index(&index); println!(" Number of edges: {edge_count}"); @@ -200,6 +220,7 @@ where } } println!(); + Ok(()) } /// Validate the triangulation and report results @@ -369,11 +390,24 @@ where }) .collect(); + if validation_times.is_empty() { + #[cfg(feature = "diagnostics")] + tracing::warn!("validation timing skipped: no validation runs were recorded"); + return; + } + let len_u32 = u32::try_from(validation_times.len()).unwrap_or(1u32); - let avg_validation_time: std::time::Duration = - validation_times.iter().sum::() / len_u32; - let min_validation_time = *validation_times.iter().min().unwrap(); - let max_validation_time = *validation_times.iter().max().unwrap(); + let avg_validation_time: Duration = validation_times.iter().sum::() / len_u32; + let Some(min_validation_time) = validation_times.iter().min().copied() else { + #[cfg(feature = "diagnostics")] + tracing::warn!("validation timing skipped: no minimum validation time was recorded"); + return; + }; + let Some(max_validation_time) = validation_times.iter().max().copied() else { + #[cfg(feature = "diagnostics")] + tracing::warn!("validation timing skipped: no maximum validation time was recorded"); + return; + }; println!(" Full Validation Performance (Levels 1–4, 5 runs):"); println!(" • Average time: {avg_validation_time:?}"); @@ -390,8 +424,7 @@ where .collect(); let len_u32_boundary = u32::try_from(boundary_times.len()).unwrap_or(1u32); - let avg_boundary_time: std::time::Duration = - boundary_times.iter().sum::() / len_u32_boundary; + let avg_boundary_time: Duration = boundary_times.iter().sum::() / len_u32_boundary; println!("\n Boundary Computation Performance (3 runs):"); println!(" • Average time: {avg_boundary_time:?}"); diff --git a/justfile b/justfile index d0ac0485..63cdb8a9 100644 --- a/justfile +++ b/justfile @@ -523,8 +523,6 @@ verify-expect-counts: echo "✓ $label: $actual" } - check_count 'examples/**/*.rs .expect(' 35 '\.expect\(' examples - check_count 'benches/**/*.rs .expect(' 57 '\.expect\(' benches check_count 'src/**/*.rs doc-comment .expect(' 17 '^\s*//[/!].*\.expect\(' src # Pre-publish validation: checks crates.io metadata rules that cargo publish --dry-run does NOT catch diff --git a/semgrep.yaml b/semgrep.yaml index 5dbf3c74..bc3609ef 100644 --- a/semgrep.yaml +++ b/semgrep.yaml @@ -212,6 +212,35 @@ rules: ... } + - id: delaunay.rust.no-public-surface-unwrap-panic + languages: + - rust + severity: WARNING + message: >- + Use typed Result handling in public examples, benchmarks, and public API + tests instead of unwrap/expect/panic. + metadata: + category: correctness + rationale: >- + User-facing examples, benchmarks, and public API tests are copied as + usage patterns. They should preserve typed error information instead + of erasing failures behind unwrap, expect, or panic. + paths: + include: + - "/examples/**/*.rs" + - "/benches/**/*.rs" + - "/tests/allocation_api.rs" + - "/tests/delaunay_public_api_coverage.rs" + - "/tests/prelude_exports.rs" + - "/tests/public_*.rs" + - "/tests/semgrep/src/project_rules/**/*.rs" + exclude: + - "/tests/semgrep/**" + pattern-either: + - pattern: $VALUE.unwrap() + - pattern: $VALUE.expect(...) + - pattern: panic!(...) + - id: delaunay.rust.public-error-enums-non-exhaustive languages: - generic @@ -272,7 +301,7 @@ rules: languages: - rust severity: WARNING - message: "Use a typed error enum in production src/ instead of Box, &dyn Error, or anyhow::Error." + message: "Use a typed error enum instead of Box, &dyn Error, or anyhow::Error." metadata: category: maintainability rationale: >- @@ -281,10 +310,13 @@ rules: paths: include: - "/src/**/*.rs" + - "/examples/**/*.rs" + - "/benches/**/*.rs" + - "/tests/allocation_api.rs" + - "/tests/delaunay_public_api_coverage.rs" + - "/tests/prelude_exports.rs" + - "/tests/public_*.rs" exclude: - - "/tests/**" - - "/examples/**" - - "/benches/**" - "/tests/semgrep/**" patterns: # Regex is deliberate here: Rust AST patterns over-match associated diff --git a/src/core/algorithms/flips.rs b/src/core/algorithms/flips.rs index 44cbe72a..88af3654 100644 --- a/src/core/algorithms/flips.rs +++ b/src/core/algorithms/flips.rs @@ -28,9 +28,11 @@ #![forbid(unsafe_code)] use crate::core::algorithms::incremental_insertion::{ - InsertionError, NeighborWiringError, external_facets_for_boundary, wire_cavity_neighbors, + CavityFillingError, HullExtensionReason, InsertionError, NeighborWiringError, + TdsConstructionFailure, TdsValidationFailure, external_facets_for_boundary, + wire_cavity_neighbors, }; -use crate::core::algorithms::locate::{ConflictError, extract_cavity_boundary}; +use crate::core::algorithms::locate::{ConflictError, LocateError, extract_cavity_boundary}; use crate::core::cell::{Cell, CellValidationError}; use crate::core::collections::{ CellKeyBuffer, FastHashMap, FastHashSet, FastHasher, MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer, @@ -38,9 +40,9 @@ use crate::core::collections::{ use crate::core::edge::EdgeKey; use crate::core::facet::{AllFacetsIter, FacetHandle, facet_key_from_vertices}; use crate::core::operations::TopologicalOperation; -use crate::core::tds::{CellKey, Tds, TdsConstructionError, TdsError, VertexKey}; +use crate::core::tds::{CellKey, EntityKind, Tds, VertexKey}; use crate::core::traits::data_type::DataType; -use crate::core::triangulation::{TopologyGuarantee, Triangulation}; +use crate::core::triangulation::{TopologyGuarantee, Triangulation, TriangulationValidationError}; use crate::core::util::stable_hash_u64_slice; use crate::core::vertex::Vertex; use crate::geometry::kernel::Kernel; @@ -444,7 +446,7 @@ where let cell = Cell::new(vertices, None)?; let cell_key = trial.insert_cell_with_mapping(cell).map_err(|source| { FlipMutationError::CellInsertion { - source: Box::new(source), + source: source.into(), } })?; new_cells.push(cell_key); @@ -460,6 +462,15 @@ where trial.remove_cells_by_keys(removed_cells); + if let Err(source) = trial.is_valid() { + return Err(FlipMutationError::TrialValidation { + k_move, + direction, + source: source.into(), + } + .into()); + } + debug_assert!( trial.is_coherently_oriented(), "TDS coherent orientation invariant violated after bistellar flip (k={k_move}, direction={direction:?})", @@ -1787,6 +1798,387 @@ pub enum FlipContextError { MissingRemovedCellFrame, } +/// Non-recursive summary of a flip error that reached another flip error path. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum FlipFailureKind { + /// Flips are not supported for this dimension. + #[error("unsupported dimension")] + UnsupportedDimension, + /// Boundary facet. + #[error("boundary facet")] + BoundaryFacet, + /// Missing cell. + #[error("missing cell")] + MissingCell, + /// Missing vertex. + #[error("missing vertex")] + MissingVertex, + /// Missing neighbor. + #[error("missing neighbor")] + MissingNeighbor, + /// Invalid facet adjacency. + #[error("invalid facet adjacency")] + InvalidFacetAdjacency, + /// Invalid facet index. + #[error("invalid facet index")] + InvalidFacetIndex, + /// Invalid ridge index. + #[error("invalid ridge index")] + InvalidRidgeIndex, + /// Invalid ridge adjacency. + #[error("invalid ridge adjacency")] + InvalidRidgeAdjacency, + /// Invalid ridge multiplicity. + #[error("invalid ridge multiplicity")] + InvalidRidgeMultiplicity, + /// Invalid edge multiplicity. + #[error("invalid edge multiplicity")] + InvalidEdgeMultiplicity, + /// Invalid triangle multiplicity. + #[error("invalid triangle multiplicity")] + InvalidTriangleMultiplicity, + /// Invalid edge adjacency. + #[error("invalid edge adjacency")] + InvalidEdgeAdjacency, + /// Invalid triangle adjacency. + #[error("invalid triangle adjacency")] + InvalidTriangleAdjacency, + /// Invalid vertex multiplicity. + #[error("invalid vertex multiplicity")] + InvalidVertexMultiplicity, + /// Invalid vertex adjacency. + #[error("invalid vertex adjacency")] + InvalidVertexAdjacency, + /// Invalid flip context. + #[error("invalid flip context")] + InvalidFlipContext, + /// Predicate failure. + #[error("predicate failure")] + PredicateFailure, + /// Degenerate cell. + #[error("degenerate cell")] + DegenerateCell, + /// Negative orientation. + #[error("negative orientation")] + NegativeOrientation, + /// Duplicate cell. + #[error("duplicate cell")] + DuplicateCell, + /// Non-manifold facet. + #[error("non-manifold facet")] + NonManifoldFacet, + /// Inserted simplex already exists. + #[error("inserted simplex already exists")] + InsertedSimplexAlreadyExists, + /// Cell creation failed. + #[error("cell creation")] + CellCreation, + /// Neighbor wiring failed. + #[error("neighbor wiring")] + NeighborWiring, + /// Trial TDS validation failed before committing a flip. + #[error("trial validation")] + TrialValidation, + /// Neighbor wiring reached a validation failure. + #[error("wiring validation")] + WiringValidation, + /// Neighbor wiring reached a Delaunay repair failure. + #[error("Delaunay repair failed")] + DelaunayRepairFailed, + /// TDS mutation failed. + #[error("TDS mutation")] + TdsMutation, +} + +/// Non-recursive summary of a cavity-filling error at the flip wiring boundary. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum FlipNeighborCavityFailureKind { + /// Boundary cell was missing. + #[error("missing boundary cell")] + MissingBoundaryCell, + /// Inserted vertex was missing. + #[error("missing inserted vertex")] + MissingInsertedVertex, + /// Boundary cell had the wrong arity. + #[error("wrong cell arity")] + WrongCellArity, + /// Facet index was invalid. + #[error("invalid facet index")] + InvalidFacetIndex, + /// Replacement cell creation failed. + #[error("cell creation")] + CellCreation, + /// Replacement cell insertion failed. + #[error("cell insertion")] + CellInsertion, + /// Initial simplex construction failed. + #[error("initial simplex construction")] + InitialSimplexConstruction, + /// Rebuilt TDS lost the inserted vertex. + #[error("rebuilt vertex missing")] + RebuiltVertexMissing, + /// Conflict region was empty. + #[error("empty conflict region")] + EmptyConflictRegion, + /// Cavity boundary was empty. + #[error("empty boundary")] + EmptyBoundary, + /// Facet sharing remained invalid after repair. + #[error("invalid facet sharing after repair")] + InvalidFacetSharingAfterRepair, + /// Neighbor rebuild failed. + #[error("neighbor rebuild")] + NeighborRebuild, + /// Perturbation scale conversion failed. + #[error("perturbation scale conversion")] + PerturbationScaleConversion, + /// Degenerate insertion location is unsupported. + #[error("unsupported degenerate location")] + UnsupportedDegenerateLocation, + /// Fan filling produced no cells. + #[error("empty fan triangulation")] + EmptyFanTriangulation, +} + +impl From<&CavityFillingError> for FlipNeighborCavityFailureKind { + fn from(source: &CavityFillingError) -> Self { + match source { + CavityFillingError::MissingBoundaryCell { .. } => Self::MissingBoundaryCell, + CavityFillingError::MissingInsertedVertex { .. } => Self::MissingInsertedVertex, + CavityFillingError::WrongCellArity { .. } => Self::WrongCellArity, + CavityFillingError::InvalidFacetIndex { .. } => Self::InvalidFacetIndex, + CavityFillingError::CellCreation { .. } => Self::CellCreation, + CavityFillingError::CellInsertion { .. } => Self::CellInsertion, + CavityFillingError::InitialSimplexConstruction { .. } => { + Self::InitialSimplexConstruction + } + CavityFillingError::RebuiltVertexMissing { .. } => Self::RebuiltVertexMissing, + CavityFillingError::EmptyConflictRegion { .. } => Self::EmptyConflictRegion, + CavityFillingError::EmptyBoundary { .. } => Self::EmptyBoundary, + CavityFillingError::InvalidFacetSharingAfterRepair { .. } => { + Self::InvalidFacetSharingAfterRepair + } + CavityFillingError::NeighborRebuild { .. } => Self::NeighborRebuild, + CavityFillingError::PerturbationScaleConversion { .. } => { + Self::PerturbationScaleConversion + } + CavityFillingError::UnsupportedDegenerateLocation { .. } => { + Self::UnsupportedDegenerateLocation + } + CavityFillingError::EmptyFanTriangulation => Self::EmptyFanTriangulation, + } + } +} + +impl From for FlipNeighborCavityFailureKind { + fn from(source: CavityFillingError) -> Self { + Self::from(&source) + } +} + +/// Non-recursive summary of a hull-extension error at the flip wiring boundary. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum FlipNeighborHullExtensionFailureKind { + /// No visible facets were found. + #[error("no visible facets")] + NoVisibleFacets, + /// Visible facets formed an invalid patch. + #[error("invalid patch")] + InvalidPatch, + /// Geometric predicate failed. + #[error("predicate failed")] + PredicateFailed, + /// Lower-layer TDS error. + #[error("TDS")] + Tds, + /// Other hull-extension failure. + #[error("other")] + Other, +} + +impl From<&HullExtensionReason> for FlipNeighborHullExtensionFailureKind { + fn from(source: &HullExtensionReason) -> Self { + match source { + HullExtensionReason::NoVisibleFacets => Self::NoVisibleFacets, + HullExtensionReason::InvalidPatch { .. } => Self::InvalidPatch, + HullExtensionReason::PredicateFailed(_) => Self::PredicateFailed, + HullExtensionReason::Tds(_) => Self::Tds, + HullExtensionReason::Other { .. } => Self::Other, + } + } +} + +impl From for FlipNeighborHullExtensionFailureKind { + fn from(source: HullExtensionReason) -> Self { + Self::from(&source) + } +} + +/// Non-recursive summary of a Delaunay validation error at the flip wiring boundary. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum FlipNeighborDelaunayValidationFailureKind { + /// Lower-layer TDS validation failed. + #[error("TDS")] + Tds, + /// Lower-layer topology validation failed. + #[error("triangulation")] + Triangulation, + /// Delaunay verification failed. + #[error("verification failed")] + VerificationFailed, + /// Legacy repair validation failed. + #[error("repair failed")] + RepairFailed, + /// Repair operation validation failed. + #[error("repair operation failed")] + RepairOperationFailed, +} + +impl From<&crate::triangulation::delaunay::DelaunayTriangulationValidationError> + for FlipNeighborDelaunayValidationFailureKind +{ + fn from(source: &crate::triangulation::delaunay::DelaunayTriangulationValidationError) -> Self { + match source { + crate::triangulation::delaunay::DelaunayTriangulationValidationError::Tds(_) => { + Self::Tds + } + crate::triangulation::delaunay::DelaunayTriangulationValidationError::Triangulation( + _, + ) => Self::Triangulation, + crate::triangulation::delaunay::DelaunayTriangulationValidationError::VerificationFailed { + .. + } => Self::VerificationFailed, + crate::triangulation::delaunay::DelaunayTriangulationValidationError::RepairFailed { + .. + } => Self::RepairFailed, + crate::triangulation::delaunay::DelaunayTriangulationValidationError::RepairOperationFailed { + .. + } => Self::RepairOperationFailed, + } + } +} + +impl From + for FlipNeighborDelaunayValidationFailureKind +{ + fn from(source: crate::triangulation::delaunay::DelaunayTriangulationValidationError) -> Self { + Self::from(&source) + } +} + +/// Compact repair diagnostics preserved when embedding repair failures in flip wiring errors. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FlipNeighborRepairDiagnostics { + /// Number of queued items checked. + pub facets_checked: usize, + /// Number of flips performed. + pub flips_performed: usize, + /// Maximum queue length observed. + pub max_queue_len: usize, + /// Count of ambiguous predicate evaluations. + pub ambiguous_predicates: usize, + /// Count of predicate failures. + pub predicate_failures: usize, + /// Count of detected flip cycles. + pub cycle_detections: usize, + /// Attempt number. + pub attempt: usize, + /// Queue ordering policy used for this attempt. + pub queue_order: RepairQueueOrder, +} + +impl fmt::Display for FlipNeighborRepairDiagnostics { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "checked={}, flips={}, max_queue={}, ambiguous={}, predicate_failures={}, cycles={}, attempt={}, order={:?}", + self.facets_checked, + self.flips_performed, + self.max_queue_len, + self.ambiguous_predicates, + self.predicate_failures, + self.cycle_detections, + self.attempt, + self.queue_order + ) + } +} + +impl From for FlipNeighborRepairDiagnostics { + fn from(source: DelaunayRepairDiagnostics) -> Self { + Self { + facets_checked: source.facets_checked, + flips_performed: source.flips_performed, + max_queue_len: source.max_queue_len, + ambiguous_predicates: source.ambiguous_predicates, + predicate_failures: source.predicate_failures, + cycle_detections: source.cycle_detections, + attempt: source.attempt, + queue_order: source.queue_order, + } + } +} + +/// Non-recursive reason Delaunay repair reached flip neighbor wiring. +#[derive(Clone, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum FlipNeighborRepairFailure { + /// Repair did not converge within the flip budget. + #[error("repair did not converge after {max_flips} flips ({diagnostics})")] + NonConvergent { + /// Maximum flips allowed. + max_flips: usize, + /// Diagnostics captured during the failed attempt. + diagnostics: FlipNeighborRepairDiagnostics, + }, + /// Repair completed but left a Delaunay violation. + #[error("repair postcondition failed: {message}")] + PostconditionFailed { + /// Additional context describing the postcondition failure. + message: String, + }, + /// Post-repair verification could not evaluate a local flip predicate. + #[error("repair verification failed during {context}: {source_kind}")] + VerificationFailed { + /// Verification phase that failed. + context: &'static str, + /// Non-recursive class of the underlying flip error. + source_kind: FlipFailureKind, + }, + /// Repair completed but orientation canonicalization failed. + #[error("repair orientation canonicalization failed: {message}")] + OrientationCanonicalizationFailed { + /// Additional context describing the canonicalization failure. + message: String, + }, + /// Flip-based repair is not admissible under the current topology guarantee. + #[error("repair requires {required:?} topology, found {found:?}: {message}")] + InvalidTopology { + /// Required topology guarantee. + required: TopologyGuarantee, + /// Actual topology guarantee. + found: TopologyGuarantee, + /// Additional context for the mismatch. + message: &'static str, + }, + /// Heuristic rebuild failed during advanced repair. + #[error("heuristic rebuild failed: {message}")] + HeuristicRebuildFailed { + /// Additional context for the rebuild failure. + message: String, + }, + /// Underlying flip error. + #[error("flip error: {source_kind}")] + Flip { + /// Non-recursive class of the underlying flip error. + source_kind: FlipFailureKind, + }, +} + /// Structured reason neighbor wiring failed during flip application. #[derive(Clone, Debug, Error, PartialEq, Eq)] #[non_exhaustive] @@ -1818,13 +2210,69 @@ pub enum FlipNeighborWiringError { TopologyValidation { /// Underlying TDS validation error. #[source] - source: TdsError, + source: TdsValidationFailure, + }, + /// Conflict-region extraction reached flip neighbor wiring. + #[error("conflict-region error reached flip neighbor wiring: {source}")] + ConflictRegion { + /// Underlying conflict-region error. + #[source] + source: ConflictError, + }, + /// Point-location failure reached flip neighbor wiring. + #[error("point-location error reached flip neighbor wiring: {source}")] + Location { + /// Underlying point-location error. + #[source] + source: LocateError, + }, + /// Cavity filling failed while preparing flip neighbor wiring. + #[error("cavity filling error reached flip neighbor wiring: {reason}")] + CavityFilling { + /// Structured cavity-filling reason. + reason: FlipNeighborCavityFailureKind, + }, + /// Hull extension failed while preparing flip neighbor wiring. + #[error("hull extension error reached flip neighbor wiring: {reason}")] + HullExtension { + /// Structured hull-extension reason. + reason: FlipNeighborHullExtensionFailureKind, + }, + /// Delaunay validation failed while preparing flip neighbor wiring. + #[error("Delaunay validation error reached flip neighbor wiring: {reason}")] + DelaunayValidation { + /// Structured validation reason. + reason: FlipNeighborDelaunayValidationFailureKind, + }, + /// Delaunay repair failed while preparing flip neighbor wiring. + #[error("Delaunay repair error reached flip neighbor wiring: {reason}")] + DelaunayRepair { + /// Structured non-recursive repair reason. + #[source] + reason: FlipNeighborRepairFailure, + }, + /// Duplicate coordinates reached flip neighbor wiring. + #[error("duplicate coordinates reached flip neighbor wiring: {coordinates}")] + DuplicateCoordinates { + /// Duplicate coordinate tuple. + coordinates: String, + }, + /// Duplicate UUID reached flip neighbor wiring. + #[error("duplicate UUID reached flip neighbor wiring: {entity:?} {uuid}")] + DuplicateUuid { + /// Entity kind. + entity: EntityKind, + /// Duplicated UUID. + uuid: uuid::Uuid, }, - /// An unexpected insertion-layer error reached flip neighbor wiring. - #[error("unexpected neighbor wiring error: {message}")] - Unexpected { - /// Display form of the unexpected error. + /// Level 3 topology validation failed while preparing flip neighbor wiring. + #[error("topology validation error reached flip neighbor wiring: {message}: {source}")] + TopologyValidationFailed { + /// High-level insertion context. message: String, + /// Underlying topology validation error. + #[source] + source: TriangulationValidationError, }, } @@ -1839,10 +2287,33 @@ impl From for FlipNeighborWiringError { facet_hash, cell_count, }, - InsertionError::TopologyValidation(source) => Self::TopologyValidation { source }, - other => Self::Unexpected { - message: other.to_string(), + InsertionError::TopologyValidation(source) => Self::TopologyValidation { + source: source.into(), + }, + InsertionError::ConflictRegion(source) => Self::ConflictRegion { source }, + InsertionError::Location(source) => Self::Location { source }, + InsertionError::CavityFilling { reason } => Self::CavityFilling { + reason: reason.into(), }, + InsertionError::HullExtension { reason } => Self::HullExtension { + reason: reason.into(), + }, + InsertionError::DelaunayValidationFailed { source } => Self::DelaunayValidation { + reason: (*source).into(), + }, + InsertionError::DelaunayRepairFailed { source, context: _ } => Self::DelaunayRepair { + reason: FlipNeighborRepairFailure::from(*source), + }, + InsertionError::DuplicateCoordinates { coordinates } => { + Self::DuplicateCoordinates { coordinates } + } + InsertionError::DuplicateUuid { entity, uuid } => Self::DuplicateUuid { entity, uuid }, + InsertionError::TopologyValidationFailed { message, source } => { + Self::TopologyValidationFailed { + message, + source: *source, + } + } } } } @@ -1856,14 +2327,27 @@ pub enum FlipMutationError { VertexInsertion { /// Underlying TDS construction error. #[source] - source: Box, + source: TdsConstructionFailure, }, /// Replacement-cell insertion failed. #[error("cell insertion failed: {source}")] CellInsertion { /// Underlying TDS construction error. #[source] - source: Box, + source: TdsConstructionFailure, + }, + /// Trial TDS validation failed before committing a flip. + #[error( + "trial TDS validation failed after bistellar flip (k={k_move}, direction={direction:?}): {source}" + )] + TrialValidation { + /// k for the attempted move. + k_move: usize, + /// Direction of the attempted move. + direction: FlipDirection, + /// Underlying TDS validation error. + #[source] + source: TdsValidationFailure, }, } @@ -2179,6 +2663,56 @@ impl From for FlipError { } } +impl From<&FlipError> for FlipFailureKind { + fn from(source: &FlipError) -> Self { + match source { + FlipError::UnsupportedDimension { .. } => Self::UnsupportedDimension, + FlipError::BoundaryFacet { .. } => Self::BoundaryFacet, + FlipError::MissingCell { .. } => Self::MissingCell, + FlipError::MissingVertex { .. } => Self::MissingVertex, + FlipError::MissingNeighbor { .. } => Self::MissingNeighbor, + FlipError::InvalidFacetAdjacency { .. } => Self::InvalidFacetAdjacency, + FlipError::InvalidFacetIndex { .. } => Self::InvalidFacetIndex, + FlipError::InvalidRidgeIndex { .. } => Self::InvalidRidgeIndex, + FlipError::InvalidRidgeAdjacency { .. } => Self::InvalidRidgeAdjacency, + FlipError::InvalidRidgeMultiplicity { .. } => Self::InvalidRidgeMultiplicity, + FlipError::InvalidEdgeMultiplicity { .. } => Self::InvalidEdgeMultiplicity, + FlipError::InvalidTriangleMultiplicity { .. } => Self::InvalidTriangleMultiplicity, + FlipError::InvalidEdgeAdjacency { .. } => Self::InvalidEdgeAdjacency, + FlipError::InvalidTriangleAdjacency { .. } => Self::InvalidTriangleAdjacency, + FlipError::InvalidVertexMultiplicity { .. } => Self::InvalidVertexMultiplicity, + FlipError::InvalidVertexAdjacency { .. } => Self::InvalidVertexAdjacency, + FlipError::InvalidFlipContext { .. } => Self::InvalidFlipContext, + FlipError::PredicateFailure { .. } => Self::PredicateFailure, + FlipError::DegenerateCell => Self::DegenerateCell, + FlipError::NegativeOrientation { .. } => Self::NegativeOrientation, + FlipError::DuplicateCell => Self::DuplicateCell, + FlipError::NonManifoldFacet => Self::NonManifoldFacet, + FlipError::InsertedSimplexAlreadyExists { .. } => Self::InsertedSimplexAlreadyExists, + FlipError::CellCreation(_) => Self::CellCreation, + FlipError::NeighborWiring { reason } => match reason { + FlipNeighborWiringError::TopologyValidation { .. } + | FlipNeighborWiringError::DelaunayValidation { .. } + | FlipNeighborWiringError::TopologyValidationFailed { .. } => { + Self::WiringValidation + } + FlipNeighborWiringError::DelaunayRepair { .. } => Self::DelaunayRepairFailed, + _ => Self::NeighborWiring, + }, + FlipError::TdsMutation { + reason: FlipMutationError::TrialValidation { .. }, + } => Self::TrialValidation, + FlipError::TdsMutation { .. } => Self::TdsMutation, + } + } +} + +impl From for FlipFailureKind { + fn from(source: FlipError) -> Self { + Self::from(&source) + } +} + /// Information about a successful flip. /// /// # Examples @@ -2612,6 +3146,47 @@ pub enum DelaunayRepairError { Flip(#[from] FlipError), } +impl From for FlipNeighborRepairFailure { + fn from(source: DelaunayRepairError) -> Self { + match source { + DelaunayRepairError::NonConvergent { + max_flips, + diagnostics, + } => Self::NonConvergent { + max_flips, + diagnostics: (*diagnostics).into(), + }, + DelaunayRepairError::PostconditionFailed { message } => { + Self::PostconditionFailed { message } + } + DelaunayRepairError::VerificationFailed { context, source } => { + Self::VerificationFailed { + context, + source_kind: FlipFailureKind::from(source.as_ref()), + } + } + DelaunayRepairError::OrientationCanonicalizationFailed { message } => { + Self::OrientationCanonicalizationFailed { message } + } + DelaunayRepairError::InvalidTopology { + required, + found, + message, + } => Self::InvalidTopology { + required, + found, + message, + }, + DelaunayRepairError::HeuristicRebuildFailed { message } => { + Self::HeuristicRebuildFailed { message } + } + DelaunayRepairError::Flip(source) => Self::Flip { + source_kind: FlipFailureKind::from(source), + }, + } + } +} + /// Build flip context for a k=2 (facet) flip. /// /// # Errors @@ -3519,7 +4094,7 @@ where let vertex_key = tds.insert_vertex_with_mapping(vertex).map_err(|source| { FlipMutationError::VertexInsertion { - source: Box::new(source), + source: source.into(), } })?; @@ -8511,15 +9086,16 @@ mod tests { #[derive(Debug, Clone, PartialEq, Eq)] struct TopologySnapshot { - vertex_uuids: Vec, - cell_vertex_uuids: Vec>, + vertices: Vec, + cell_vertices: Vec>, + cell_neighbors: Vec>>, } fn snapshot_topology(tds: &Tds) -> TopologySnapshot { - let mut vertex_uuids: Vec = tds.vertices().map(|(_, vertex)| vertex.uuid()).collect(); - vertex_uuids.sort(); + let mut vertices: Vec = tds.vertices().map(|(_, vertex)| vertex.uuid()).collect(); + vertices.sort(); - let mut cell_vertex_uuids: Vec> = tds + let mut cell_vertices: Vec> = tds .cells() .map(|(_, cell)| { let mut uuids: Vec = cell @@ -8531,14 +9107,134 @@ mod tests { uuids }) .collect(); - cell_vertex_uuids.sort(); + cell_vertices.sort(); + + let cell_neighbors = snapshot_neighbors(tds); TopologySnapshot { - vertex_uuids, - cell_vertex_uuids, + vertices, + cell_vertices, + cell_neighbors, + } + } + + fn snapshot_neighbors(tds: &Tds) -> Vec>> { + let mut cell_neighbors: Vec>> = tds + .cells() + .map(|(_, cell)| { + let mut neighbors: Vec> = cell + .neighbors() + .map(|neighbor_keys| { + neighbor_keys + .iter() + .map(|neighbor| { + neighbor + .and_then(|neighbor_key| tds.cell(neighbor_key).map(Cell::uuid)) + }) + .collect() + }) + .unwrap_or_default(); + neighbors.sort(); + neighbors + }) + .collect(); + cell_neighbors.sort(); + cell_neighbors + } + + fn snapshot_incidence(tds: &Tds) -> Vec<(Uuid, Option)> { + let mut incident_cells: Vec<(Uuid, Option)> = tds + .vertices() + .map(|(_, vertex)| { + ( + vertex.uuid(), + vertex + .incident_cell + .and_then(|cell_key| tds.cell(cell_key).map(Cell::uuid)), + ) + }) + .collect(); + incident_cells.sort(); + incident_cells + } + + fn assert_same_vertex_cell_topology(actual: &TopologySnapshot, expected: &TopologySnapshot) { + assert_eq!(actual.vertices, expected.vertices); + assert_eq!(actual.cell_vertices, expected.cell_vertices); + } + + fn insert_translated_simplex( + tds: &mut Tds, + offset: f64, + ) -> (Vec, CellKey) { + let mut vertices = Vec::with_capacity(D + 1); + vertices.push( + tds.insert_vertex_with_mapping(vertex!([offset; D])) + .unwrap(), + ); + + for axis in 0..D { + let mut coords = [offset; D]; + coords[axis] += 1.0; + vertices.push(tds.insert_vertex_with_mapping(vertex!(coords)).unwrap()); } + + let cell_key = tds + .insert_cell_with_mapping(Cell::new(vertices.clone(), None).unwrap()) + .unwrap(); + (vertices, cell_key) } + fn test_flip_trial_validation_rollback_for_dim() { + let mut tds: Tds = Tds::empty(); + let (_first_vertices, first_cell) = insert_translated_simplex(&mut tds, 0.0); + let (_second_vertices, second_cell) = insert_translated_simplex(&mut tds, 10.0); + repair_neighbor_pointers(&mut tds).unwrap(); + tds.assign_incident_cells().unwrap(); + + let isolated_vertex = tds.insert_vertex_with_mapping(vertex!([20.0; D])).unwrap(); + tds.vertex_mut(isolated_vertex).unwrap().incident_cell = Some(second_cell); + + let before = snapshot_topology(&tds); + let before_incidence = snapshot_incidence(&tds); + let denominator = f64::from(u32::try_from(D + 2).unwrap()); + let new_vertex = vertex!([1.0 / denominator; D]); + + let result = apply_bistellar_flip_k1(&mut tds, first_cell, new_vertex); + match result { + Err(FlipError::TdsMutation { + reason: FlipMutationError::TrialValidation { .. }, + }) => {} + other => panic!("expected FlipMutationError::TrialValidation, got {other:?}"), + } + + assert_eq!( + snapshot_topology(&tds), + before, + "trial.is_valid() failure must leave the original TDS unchanged" + ); + assert_eq!( + snapshot_incidence(&tds), + before_incidence, + "trial.is_valid() failure must leave incident_cell pointers unchanged" + ); + } + + macro_rules! gen_trial_validation_rollback_tests { + ($($dim:literal),+ $(,)?) => { + pastey::paste! { + $( + #[test] + fn []() { + test_flip_trial_validation_rollback_for_dim::<$dim>(); + } + )+ + } + }; + } + + gen_trial_validation_rollback_tests!(2, 3, 4, 5); + macro_rules! test_bistellar_roundtrip_dimension { ($dim:literal) => { pastey::paste! { @@ -8652,7 +9348,8 @@ mod tests { } assert!(tds.is_valid().is_ok()); - assert_eq!(snapshot_topology(&tds), before); + let after = snapshot_topology(&tds); + assert_same_vertex_cell_topology(&after, &before); } } }; @@ -8753,7 +9450,8 @@ mod tests { } assert!(tds.is_valid().is_ok()); - assert_eq!(snapshot_topology(&tds), before); + let after = snapshot_topology(&tds); + assert_same_vertex_cell_topology(&after, &before); } } }; @@ -9487,7 +10185,8 @@ mod tests { let _info_back = apply_bistellar_flip_dynamic(&mut tds, 3, &context_back).unwrap(); assert!(tds.is_valid().is_ok()); - assert_eq!(snapshot_topology(&tds), before); + let after = snapshot_topology(&tds); + assert_same_vertex_cell_topology(&after, &before); } } @@ -10217,6 +10916,173 @@ mod tests { assert_ne!(ridge_4, ridge_5); } + fn sample_tds_validation_failure() -> TdsValidationFailure { + TdsValidationFailure::InvalidNeighbors { + message: "synthetic neighbor mismatch".to_string(), + } + } + + fn sample_repair_diagnostics() -> DelaunayRepairDiagnostics { + DelaunayRepairDiagnostics { + facets_checked: 7, + flips_performed: 3, + max_queue_len: 5, + ambiguous_predicates: 2, + ambiguous_predicate_samples: vec![11, 13], + predicate_failures: 1, + cycle_detections: 4, + cycle_signature_samples: vec![17, 19], + attempt: 2, + queue_order: RepairQueueOrder::Lifo, + } + } + + #[test] + fn test_flip_failure_kind_preserves_nested_validation_and_repair_reasons() { + let trial_error = FlipError::from(FlipMutationError::TrialValidation { + k_move: 2, + direction: FlipDirection::Forward, + source: sample_tds_validation_failure(), + }); + assert_eq!( + FlipFailureKind::from(&trial_error), + FlipFailureKind::TrialValidation + ); + + let wiring_validation = FlipError::from(FlipNeighborWiringError::TopologyValidation { + source: sample_tds_validation_failure(), + }); + assert_eq!( + FlipFailureKind::from(&wiring_validation), + FlipFailureKind::WiringValidation + ); + + let repair_reason = + FlipNeighborRepairFailure::from(DelaunayRepairError::VerificationFailed { + context: "post-repair verification", + source: Box::new(trial_error), + }); + match &repair_reason { + FlipNeighborRepairFailure::VerificationFailed { + context, + source_kind, + } => { + assert_eq!(*context, "post-repair verification"); + assert_eq!(*source_kind, FlipFailureKind::TrialValidation); + } + other => panic!("expected verification failure, got {other:?}"), + } + + let wiring_repair = FlipError::from(FlipNeighborWiringError::DelaunayRepair { + reason: repair_reason, + }); + assert_eq!( + FlipFailureKind::from(&wiring_repair), + FlipFailureKind::DelaunayRepairFailed + ); + } + + #[test] + fn test_flip_neighbor_conversion_kinds_cover_insertion_suberrors() { + let cavity_kind = FlipNeighborCavityFailureKind::from( + &CavityFillingError::UnsupportedDegenerateLocation { + location: crate::core::algorithms::locate::LocateResult::Outside, + }, + ); + assert_eq!( + cavity_kind, + FlipNeighborCavityFailureKind::UnsupportedDegenerateLocation + ); + assert_eq!(cavity_kind.to_string(), "unsupported degenerate location"); + + let hull_kind = + FlipNeighborHullExtensionFailureKind::from(&HullExtensionReason::InvalidPatch { + details: "non-manifold visible patch".to_string(), + }); + assert_eq!( + hull_kind, + FlipNeighborHullExtensionFailureKind::InvalidPatch + ); + assert_eq!(hull_kind.to_string(), "invalid patch"); + + let validation_kind = FlipNeighborDelaunayValidationFailureKind::from( + &crate::triangulation::delaunay::DelaunayTriangulationValidationError::RepairOperationFailed { + operation: crate::triangulation::delaunay::DelaunayRepairOperation::VertexRemoval, + source: Box::new(DelaunayRepairError::InvalidTopology { + required: TopologyGuarantee::PLManifold, + found: TopologyGuarantee::Pseudomanifold, + message: "repair requires PL topology", + }), + }, + ); + assert_eq!( + validation_kind, + FlipNeighborDelaunayValidationFailureKind::RepairOperationFailed + ); + assert_eq!(validation_kind.to_string(), "repair operation failed"); + + let repair_wiring = FlipNeighborWiringError::from(InsertionError::DelaunayRepairFailed { + source: Box::new(DelaunayRepairError::InvalidTopology { + required: TopologyGuarantee::PLManifold, + found: TopologyGuarantee::Pseudomanifold, + message: "repair requires PL topology", + }), + context: "post-insertion repair".to_string(), + }); + match repair_wiring { + FlipNeighborWiringError::DelaunayRepair { + reason: + FlipNeighborRepairFailure::InvalidTopology { + required, + found, + message, + }, + } => { + assert_eq!(required, TopologyGuarantee::PLManifold); + assert_eq!(found, TopologyGuarantee::Pseudomanifold); + assert_eq!(message, "repair requires PL topology"); + } + other => panic!("expected preserved Delaunay repair reason, got {other:?}"), + } + } + + #[test] + fn test_flip_neighbor_repair_diagnostics_preserve_summary_fields() { + let diagnostics = sample_repair_diagnostics(); + let summary = FlipNeighborRepairDiagnostics::from(diagnostics.clone()); + + assert_eq!(summary.facets_checked, diagnostics.facets_checked); + assert_eq!(summary.flips_performed, diagnostics.flips_performed); + assert_eq!(summary.max_queue_len, diagnostics.max_queue_len); + assert_eq!( + summary.ambiguous_predicates, + diagnostics.ambiguous_predicates + ); + assert_eq!(summary.predicate_failures, diagnostics.predicate_failures); + assert_eq!(summary.cycle_detections, diagnostics.cycle_detections); + assert_eq!(summary.attempt, diagnostics.attempt); + assert_eq!(summary.queue_order, diagnostics.queue_order); + assert_eq!( + summary.to_string(), + "checked=7, flips=3, max_queue=5, ambiguous=2, predicate_failures=1, cycles=4, attempt=2, order=Lifo" + ); + + let non_convergent = FlipNeighborRepairFailure::from(DelaunayRepairError::NonConvergent { + max_flips: 42, + diagnostics: Box::new(diagnostics), + }); + match non_convergent { + FlipNeighborRepairFailure::NonConvergent { + max_flips, + diagnostics, + } => { + assert_eq!(max_flips, 42); + assert_eq!(diagnostics.flips_performed, 3); + } + other => panic!("expected non-convergent repair summary, got {other:?}"), + } + } + #[test] fn test_delaunay_repair_error_partial_eq() { let post_test = DelaunayRepairError::PostconditionFailed { diff --git a/src/core/algorithms/incremental_insertion.rs b/src/core/algorithms/incremental_insertion.rs index 7954834a..ed546ba2 100644 --- a/src/core/algorithms/incremental_insertion.rs +++ b/src/core/algorithms/incremental_insertion.rs @@ -602,7 +602,7 @@ pub enum NeighborRebuildError { /// }; /// assert!(matches!(err, CavityFillingError::InvalidFacetIndex { .. })); /// ``` -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] #[non_exhaustive] pub enum CavityFillingError { /// A boundary facet references a cell that is no longer present. @@ -874,7 +874,7 @@ pub enum NeighborWiringError { /// }; /// assert!(matches!(err, InsertionError::DuplicateCoordinates { .. })); /// ``` -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] #[non_exhaustive] pub enum InsertionError { /// Conflict region finding failed diff --git a/src/core/collections/aliases.rs b/src/core/collections/aliases.rs index b491772e..6ffd5e74 100644 --- a/src/core/collections/aliases.rs +++ b/src/core/collections/aliases.rs @@ -1,3 +1,8 @@ +//! Core collection aliases used throughout triangulation storage and algorithms. +//! +//! These aliases centralize the crate's hasher, slotmap, and small-buffer choices so +//! public APIs and internal algorithms use consistent collection types. + use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet, FxHasher}; use slotmap::DenseSlotMap; use smallvec::SmallVec; diff --git a/src/core/collections/buffers.rs b/src/core/collections/buffers.rs index ac3f5900..50ede309 100644 --- a/src/core/collections/buffers.rs +++ b/src/core/collections/buffers.rs @@ -1,3 +1,8 @@ +//! Stack-friendly buffer aliases for topology and geometry algorithms. +//! +//! The aliases in this module document the expected cardinality of common +//! intermediate collections while keeping hot paths allocation-conscious. + use super::{MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer, Uuid}; use crate::core::facet::FacetHandle; use crate::core::tds::{CellKey, VertexKey}; @@ -244,12 +249,6 @@ mod tests { fn test_cell_vertex_buffer_stack_allocation_boundary() { let mut vertex_slots: SlotMap = SlotMap::default(); - eprintln!( - "inline capacity: {}, uuid size: {} bytes", - MAX_PRACTICAL_DIMENSION_SIZE, - std::mem::size_of::() - ); - // Test D=7 case: 8 vertices (D+1) should stay on stack // MAX_PRACTICAL_DIMENSION_SIZE is 8, so inline capacity is 8 let mut buffer_d7: CellVertexBuffer = CellVertexBuffer::new(); diff --git a/src/core/collections/helpers.rs b/src/core/collections/helpers.rs index e700d165..32901355 100644 --- a/src/core/collections/helpers.rs +++ b/src/core/collections/helpers.rs @@ -1,3 +1,8 @@ +//! Constructors for optimized hash maps, sets, and small buffers. +//! +//! These helpers keep allocation and hasher choices explicit at call sites without +//! repeating the concrete collection aliases throughout the codebase. + use super::{FastBuildHasher, FastHashMap, FastHashSet, SmallBuffer}; // ============================================================================= @@ -132,12 +137,6 @@ mod tests { #[test] fn test_capacity_helpers() { - eprintln!( - "small_buffer_with_capacity_2: use case is facet-to-cell relationships (2 cells per facet)" - ); - eprintln!( - "small_buffer_with_capacity_16: use case is batch vertex/cell collections in higher-dimensional operations" - ); // Test hash map and set capacity helpers let map = fast_hash_map_with_capacity::(100); assert!(map.capacity() >= 100); diff --git a/src/core/collections/key_maps.rs b/src/core/collections/key_maps.rs index 35d77e70..28eea8d7 100644 --- a/src/core/collections/key_maps.rs +++ b/src/core/collections/key_maps.rs @@ -1,3 +1,8 @@ +//! Keyed map and set aliases for vertex, cell, and UUID lookups. +//! +//! These types describe the canonical hash-map shapes used for topology storage, +//! validation, and geometric algorithm bookkeeping. + use super::{FastHashMap, FastHashSet, Uuid}; use crate::core::tds::{CellKey, VertexKey}; @@ -19,22 +24,35 @@ pub type VertexUuidSet = FastHashSet; // UUID-KEY MAPPING TYPES // ============================================================================= -/// Optimized mapping from Vertex UUIDs to `VertexKeys` for fast UUID → Key lookups. +/// Optimized mapping from vertex [`Uuid`] values to [`VertexKey`] values for fast UUID → key lookups. /// This is the primary direction for most triangulation operations. /// /// # Optimization Rationale /// /// - **Primary Direction**: UUID → Key is the hot path in most algorithms /// - **Hash Function**: `FastHasher` provides ~2-3x faster lookups than default hasher in typical non-adversarial workloads -/// - **Use Case**: Converting vertex UUIDs to keys for `SlotMap` access +/// - **Use Case**: Converting vertex UUIDs to keys for slot-map access /// - **Performance**: O(1) average case, optimized for triangulation algorithms /// /// # Reverse Lookups /// -/// For Key → UUID lookups (less common), use direct `SlotMap` access: +/// For key → UUID lookups (less common), use direct topology access: /// ```rust /// use delaunay::prelude::triangulation::*; /// +/// # #[derive(Debug, thiserror::Error)] +/// # enum ReverseLookupExampleError { +/// # #[error(transparent)] +/// # Construction { +/// # #[from] +/// # source: DelaunayTriangulationConstructionError, +/// # }, +/// # #[error("expected at least one vertex in the triangulation")] +/// # MissingVertex, +/// # #[error("vertex key should resolve in the triangulation")] +/// # UnresolvedVertexKey, +/// # } +/// # fn main() -> Result<(), ReverseLookupExampleError> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -45,33 +63,52 @@ pub type VertexUuidSet = FastHashSet; /// DelaunayTriangulation::new_with_topology_guarantee( /// &vertices, /// TopologyGuarantee::PLManifold, -/// ) -/// .unwrap(); +/// )?; /// println!("Topology guarantee: {:?}", dt.topology_guarantee()); /// let tds = dt.tds(); /// /// // Get first vertex key and its UUID -/// let (vertex_key, _) = tds.vertices().next().unwrap(); -/// let vertex_uuid = tds.vertex(vertex_key).unwrap().uuid(); +/// let Some((vertex_key, _)) = tds.vertices().next() else { +/// return Err(ReverseLookupExampleError::MissingVertex); +/// }; +/// let Some(vertex) = tds.vertex(vertex_key) else { +/// return Err(ReverseLookupExampleError::UnresolvedVertexKey); +/// }; +/// let vertex_uuid = vertex.uuid(); +/// # Ok(()) +/// # } /// ``` pub type UuidToVertexKeyMap = FastHashMap; -/// Optimized mapping from Cell UUIDs to `CellKeys` for fast UUID → Key lookups. +/// Optimized mapping from cell [`Uuid`] values to [`CellKey`] values for fast UUID → key lookups. /// This is the primary direction for most triangulation operations. /// /// # Optimization Rationale /// /// - **Primary Direction**: UUID → Key is the hot path in neighbor assignment /// - **Hash Function**: `FastHasher` provides ~2-3x faster lookups than default hasher in typical non-adversarial workloads -/// - **Use Case**: Converting cell UUIDs to keys for `SlotMap` access +/// - **Use Case**: Converting cell UUIDs to keys for slot-map access /// - **Performance**: O(1) average case, optimized for triangulation algorithms /// /// # Reverse Lookups /// -/// For Key → UUID lookups (less common), use direct `SlotMap` access: +/// For key → UUID lookups (less common), use direct topology access: /// ```rust /// use delaunay::prelude::triangulation::*; /// +/// # #[derive(Debug, thiserror::Error)] +/// # enum ReverseLookupExampleError { +/// # #[error(transparent)] +/// # Construction { +/// # #[from] +/// # source: DelaunayTriangulationConstructionError, +/// # }, +/// # #[error("expected at least one cell in the triangulation")] +/// # MissingCell, +/// # #[error("cell key should resolve in the triangulation")] +/// # UnresolvedCellKey, +/// # } +/// # fn main() -> Result<(), ReverseLookupExampleError> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -82,14 +119,21 @@ pub type UuidToVertexKeyMap = FastHashMap; /// DelaunayTriangulation::new_with_topology_guarantee( /// &vertices, /// TopologyGuarantee::PLManifold, -/// ) -/// .unwrap(); -/// println!("Topology guarantee: {:?}", dt.topology_guarantee()); +/// )?; +/// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); /// let tds = dt.tds(); /// /// // Get first cell key and its UUID -/// let (cell_key, _) = tds.cells().next().unwrap(); -/// let cell_uuid = tds.cell(cell_key).unwrap().uuid(); +/// let Some((cell_key, _)) = tds.cells().next() else { +/// return Err(ReverseLookupExampleError::MissingCell); +/// }; +/// let Some(cell) = tds.cell(cell_key) else { +/// return Err(ReverseLookupExampleError::UnresolvedCellKey); +/// }; +/// let cell_uuid = cell.uuid(); +/// assert_eq!(tds.cell_key_from_uuid(&cell_uuid), Some(cell_key)); +/// # Ok(()) +/// # } /// ``` pub type UuidToCellKeyMap = FastHashMap; diff --git a/src/core/collections/secondary_maps.rs b/src/core/collections/secondary_maps.rs index 3eba3f2a..8d3617a8 100644 --- a/src/core/collections/secondary_maps.rs +++ b/src/core/collections/secondary_maps.rs @@ -1,3 +1,8 @@ +//! Sparse secondary-map aliases for data associated with slotmap keys. +//! +//! These aliases provide type-safe auxiliary storage keyed by cells or vertices +//! without requiring dense side arrays. + use crate::core::tds::{CellKey, VertexKey}; use slotmap::SparseSecondaryMap; diff --git a/src/core/collections/triangulation_maps.rs b/src/core/collections/triangulation_maps.rs index 5b7bc93d..5ae08d21 100644 --- a/src/core/collections/triangulation_maps.rs +++ b/src/core/collections/triangulation_maps.rs @@ -1,3 +1,8 @@ +//! Triangulation-specific map aliases for adjacency and incidence bookkeeping. +//! +//! These aliases encode the small-buffer capacities expected by common topology +//! relationships such as facets, neighbors, cells, and vertex incidence. + use super::{ CellVertexBuffer, CellVertexUuidBuffer, FacetIndex, FastHashMap, FastHashSet, MAX_PRACTICAL_DIMENSION_SIZE, NeighborBuffer, SmallBuffer, Uuid, VertexUuidSet, diff --git a/src/core/facet.rs b/src/core/facet.rs index cea5b977..1241c30f 100644 --- a/src/core/facet.rs +++ b/src/core/facet.rs @@ -555,7 +555,10 @@ where /// /// # Errors /// - /// Returns `FacetError::CellNotFoundInTriangulation` if the cell is no longer in the TDS. + /// Returns [`FacetError::CellNotFoundInTriangulation`] if the cell is no longer in the TDS, + /// [`FacetError::InvalidFacetIndex`] if the facet index is outside the cell's vertex list, + /// or [`FacetError::VertexKeyNotFoundInTriangulation`] if the opposite vertex key no longer + /// resolves to a stored vertex. /// /// # Examples /// diff --git a/src/core/tds.rs b/src/core/tds.rs index d65c5b76..cb5efa8f 100644 --- a/src/core/tds.rs +++ b/src/core/tds.rs @@ -3632,31 +3632,45 @@ where Ok(facet_to_cells) } - /// Remove duplicate cells (cells with identical vertex sets) + /// Removes duplicate cells with identical vertex sets. /// /// Returns the number of duplicate cells that were removed. /// - /// After removing duplicate cells, this method rebuilds the topology - /// (neighbor relationships and incident cells) to maintain data structure - /// invariants and prevent stale references. + /// Duplicate removal is applied to a cloned trial [`Tds`], then the + /// topology (neighbor relationships and incident cells) is rebuilt to + /// maintain data structure invariants and prevent stale references. If the + /// rebuild or validation fails, the original structure is left unchanged. + /// + /// When duplicates are present, the rollback guarantee is implemented by + /// cloning the current [`Tds`] before removal. This keeps failed mutations + /// atomic, but the snapshot cost is linear in the size of the stored + /// topology. The method therefore requires `T: Clone` so vertex coordinates + /// can be preserved in the trial structure. /// /// # Errors /// - /// Returns a `TdsMutationError` if: + /// Returns a [`TdsMutationError`] if: /// - Vertex keys cannot be retrieved for any cell (data structure corruption) /// - Neighbor assignment fails after cell removal /// - Incident cell assignment fails after cell removal + /// - Validation fails after topology rebuild /// /// # Examples /// /// ``` - /// use delaunay::prelude::tds::Tds; + /// use delaunay::prelude::tds::{Tds, TdsMutationError}; /// + /// # fn main() -> Result<(), TdsMutationError> { /// let mut tds: Tds = Tds::empty(); - /// let removed = tds.remove_duplicate_cells().unwrap(); + /// let removed = tds.remove_duplicate_cells()?; /// assert_eq!(removed, 0); + /// # Ok(()) + /// # } /// ``` - pub fn remove_duplicate_cells(&mut self) -> Result { + pub fn remove_duplicate_cells(&mut self) -> Result + where + T: Clone, + { let mut unique_cells = FastHashMap::default(); let mut cells_to_remove = CellRemovalBuffer::new(); @@ -3679,28 +3693,29 @@ where let duplicate_count = cells_to_remove.len(); - // Second pass: remove duplicate cells and their corresponding UUID mappings - for cell_key in &cells_to_remove { - if let Some(removed_cell) = self.cells.remove(*cell_key) { - // Remove from our optimized UUID-to-key mapping - self.uuid_to_cell_key.remove(&removed_cell.uuid()); - } + if duplicate_count == 0 { + return Ok(0); } - if duplicate_count > 0 { - // Rebuild topology to avoid stale references after cell removal. - // This ensures vertices don't point to removed cells via incident_cell, - // and neighbor arrays don't reference removed keys. - // - // NOTE: Both `assign_neighbors()` and `assign_incident_cells()` are full rebuilds - // across all cells/vertices (O(#cells)). This is intentionally conservative and is - // expected to be used in repair/cleanup paths rather than per-step hot loops. - self.assign_neighbors()?; - self.assign_incident_cells()?; - - // Generation already bumped by assign_neighbors(); avoid double increment + let original_generation = self.generation(); + let mut trial = self.clone(); + trial.generation = Arc::new(AtomicU64::new(original_generation)); + let removed = trial.remove_cells_by_keys(&cells_to_remove); + let rebuild_result = (|| -> Result<(), TdsMutationError> { + trial.assign_neighbors().map_err(TdsMutationError::from)?; + trial.assign_incident_cells()?; + trial.is_valid().map_err(TdsMutationError::from)?; + Ok(()) + })(); + + if let Err(error) = rebuild_result { + self.generation + .store(original_generation, Ordering::Relaxed); + return Err(error); } - Ok(duplicate_count) + + *self = trial; + Ok(removed) } // ========================================================================= @@ -7582,10 +7597,13 @@ mod tests { tds.insert_cell_with_mapping(Cell::new(vec![v0, v1, v2], None).unwrap()) .unwrap(); assert_eq!(tds.number_of_cells(), 2); + let generation_before = tds.generation(); let removed = tds.remove_duplicate_cells().unwrap(); assert_eq!(removed, 1); assert_eq!(tds.number_of_cells(), 1); + assert!(tds.generation() > generation_before); + assert!(tds.is_valid().is_ok()); } #[test] @@ -7593,9 +7611,49 @@ mod tests { let verts = initial_simplex_vertices_3d(); let dt = DelaunayTriangulation::new(&verts).unwrap(); let mut tds = dt.tds().clone(); + let generation_before = tds.generation(); let removed = tds.remove_duplicate_cells().unwrap(); assert_eq!(removed, 0); + assert_eq!(tds.generation(), generation_before); + assert!(tds.is_valid().is_ok()); + } + + #[test] + fn test_remove_duplicate_cells_rolls_back_when_rebuild_fails() { + let mut tds: Tds = Tds::empty(); + let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap(); + let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap(); + let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap(); + let v3 = tds + .insert_vertex_with_mapping(vertex!([0.0, -1.0])) + .unwrap(); + let v4 = tds + .insert_vertex_with_mapping(vertex!([0.5, -0.5])) + .unwrap(); + + // Three distinct triangles share edge v0-v1, so global neighbor + // assignment will reject the complex after duplicate removal starts. + tds.insert_cell_with_mapping(Cell::new(vec![v0, v1, v2], None).unwrap()) + .unwrap(); + tds.insert_cell_with_mapping(Cell::new(vec![v0, v1, v2], None).unwrap()) + .unwrap(); + tds.insert_cell_with_mapping(Cell::new(vec![v0, v1, v3], None).unwrap()) + .unwrap(); + tds.insert_cell_with_mapping(Cell::new(vec![v0, v1, v4], None).unwrap()) + .unwrap(); + let before = tds.clone(); + let generation_before = tds.generation(); + + let error = tds.remove_duplicate_cells().unwrap_err(); + + assert!(matches!( + error.into_inner(), + TdsError::InconsistentDataStructure { .. } + )); + assert_eq!(tds.number_of_cells(), 4); + assert_eq!(tds.generation(), generation_before); + assert_eq!(tds, before); } // ========================================================================= @@ -8089,10 +8147,13 @@ mod tests { tds.insert_cell_with_mapping(Cell::new(vec![v0, v1, v2], None).unwrap()) .unwrap(); assert_eq!(tds.number_of_cells(), 2); + let generation_before = tds.generation(); let removed = tds.remove_duplicate_cells().unwrap(); assert_eq!(removed, 1); assert_eq!(tds.number_of_cells(), 1); + assert!(tds.generation() > generation_before); + assert!(tds.is_valid().is_ok()); } // ========================================================================= diff --git a/src/geometry/algorithms/convex_hull.rs b/src/geometry/algorithms/convex_hull.rs index 3d6d5ad3..4bba7ce2 100644 --- a/src/geometry/algorithms/convex_hull.rs +++ b/src/geometry/algorithms/convex_hull.rs @@ -15,16 +15,30 @@ //! ```rust //! use delaunay::prelude::query::*; //! +//! # #[derive(Debug, thiserror::Error)] +//! # enum ExampleError { +//! # #[error(transparent)] +//! # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), +//! # #[error(transparent)] +//! # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), +//! # #[error(transparent)] +//! # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), +//! # #[error("expected tetrahedron hull to have a first facet")] +//! # MissingFacet, +//! # } +//! # fn main() -> Result<(), ExampleError> { //! let vertices = vec![ //! vertex!([0.0, 0.0, 0.0]), //! vertex!([1.0, 0.0, 0.0]), //! vertex!([0.0, 1.0, 0.0]), //! vertex!([0.0, 0.0, 1.0]), //! ]; -//! let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); -//! let hull = ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); +//! let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; +//! let hull = ConvexHull::from_triangulation(dt.as_triangulation())?; //! let outside = Point::new([2.0, 2.0, 2.0]); -//! assert!(hull.is_point_outside(&outside, dt.as_triangulation()).unwrap()); +//! assert!(hull.is_point_outside(&outside, dt.as_triangulation())?); +//! # Ok(()) +//! # } //! ``` #![forbid(unsafe_code)] @@ -242,17 +256,31 @@ pub enum ConvexHullConstructionError { /// # use delaunay::prelude::triangulation::DelaunayTriangulation; /// # use delaunay::prelude::query::ConvexHull; /// # use delaunay::vertex; +/// # #[derive(Debug, thiserror::Error)] +/// # enum ExampleError { +/// # #[error(transparent)] +/// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), +/// # #[error(transparent)] +/// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), +/// # #[error(transparent)] +/// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), +/// # #[error("expected tetrahedron hull to have a first facet")] +/// # MissingFacet, +/// # } +/// # fn main() -> Result<(), ExampleError> { /// # let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vec![ /// # vertex!([0.0, 0.0, 0.0]), /// # vertex!([1.0, 0.0, 0.0]), /// # vertex!([0.0, 1.0, 0.0]), /// # vertex!([0.0, 0.0, 1.0]), -/// # ]).unwrap(); -/// let hull = ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); +/// # ])?; +/// let hull = ConvexHull::from_triangulation(dt.as_triangulation())?; /// assert!(hull.is_valid_for_triangulation(dt.as_triangulation())); // Valid initially /// /// // Hull extension is now implemented - inserting outside points works! -/// dt.insert(vertex!([2.0, 2.0, 2.0])).unwrap(); +/// dt.insert(vertex!([2.0, 2.0, 2.0]))?; +/// # Ok(()) +/// # } /// ``` /// /// ## When to Rebuild the Hull @@ -270,23 +298,37 @@ pub enum ConvexHullConstructionError { /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// +/// # #[derive(Debug, thiserror::Error)] +/// # enum ExampleError { +/// # #[error(transparent)] +/// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), +/// # #[error(transparent)] +/// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), +/// # #[error(transparent)] +/// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), +/// # #[error("expected tetrahedron hull to have a first facet")] +/// # MissingFacet, +/// # } +/// # fn main() -> Result<(), ExampleError> { /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), -/// ]).unwrap(); +/// ])?; /// /// // Create initial hull (note: immutable binding) -/// let hull = ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); +/// let hull = ConvexHull::from_triangulation(dt.as_triangulation())?; /// assert_eq!(hull.number_of_facets(), 4); /// assert!(hull.is_valid_for_triangulation(dt.as_triangulation())); /// /// // Hull extension is now implemented - inserting outside points works! /// // Note: The hull becomes invalid after modification and needs to be recreated /// let new_vertex = vertex!([2.0, 2.0, 2.0]); -/// dt.insert(new_vertex).unwrap(); // Now works with hull extension! +/// dt.insert(new_vertex)?; // Now works with hull extension! /// assert!(!hull.is_valid_for_triangulation(dt.as_triangulation())); // Hull is stale +/// # Ok(()) +/// # } /// ``` /// /// # Type Parameters @@ -371,6 +413,18 @@ where /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Create a 3D tetrahedron /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -378,11 +432,13 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// let hull = - /// ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt.as_triangulation())?; /// /// assert_eq!(hull.number_of_facets(), 4); // Tetrahedron has 4 faces + /// # Ok(()) + /// # } /// ``` #[must_use] pub const fn number_of_facets(&self) -> usize { @@ -406,6 +462,18 @@ where /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Create a 3D tetrahedron /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -413,14 +481,16 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// let hull = - /// ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt.as_triangulation())?; /// /// // Get the first facet /// assert!(hull.facet(0).is_some()); /// // Index out of bounds returns None /// assert!(hull.facet(10).is_none()); + /// # Ok(()) + /// # } /// ``` #[must_use] pub fn facet(&self, index: usize) -> Option<&FacetHandle> { @@ -436,6 +506,20 @@ where /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error(transparent)] + /// # Facet(#[from] delaunay::prelude::tds::FacetError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Create a 3D tetrahedron /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -443,9 +527,9 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// let hull = - /// ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt.as_triangulation())?; /// /// // Iterate over all hull facets /// let facet_count = hull.facets().count(); @@ -456,9 +540,11 @@ where /// use delaunay::prelude::tds::FacetView; /// for facet_handle in hull.facets() { /// if let Ok(facet_view) = FacetView::new(&dt.tds(), facet_handle.cell_key(), facet_handle.facet_index()) { - /// assert_eq!(facet_view.vertices().unwrap().count(), 3); // 3D facets have 3 vertices + /// assert_eq!(facet_view.vertices()?.count(), 3); // 3D facets have 3 vertices /// } /// } + /// # Ok(()) + /// # } /// ``` pub fn facets(&self) -> std::slice::Iter<'_, FacetHandle> { self.hull_facets.iter() @@ -473,6 +559,18 @@ where /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Empty hull /// let empty_hull: ConvexHull, (), (), 3> = ConvexHull::default(); /// assert!(empty_hull.is_empty()); @@ -484,10 +582,12 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// let hull = - /// ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt.as_triangulation())?; /// assert!(!hull.is_empty()); + /// # Ok(()) + /// # } /// ``` #[must_use] pub const fn is_empty(&self) -> bool { @@ -505,15 +605,27 @@ where /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Create different dimensional hulls /// let vertices_2d: Vec<_> = vec![ /// vertex!([0.0, 0.0]), /// vertex!([1.0, 0.0]), /// vertex!([0.0, 1.0]), /// ]; - /// let dt_2d: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices_2d).unwrap(); + /// let dt_2d: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices_2d)?; /// let hull_2d = - /// ConvexHull::from_triangulation(dt_2d.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt_2d.as_triangulation())?; /// assert_eq!(hull_2d.dimension(), 2); /// /// let vertices_3d: Vec<_> = vec![ @@ -522,10 +634,12 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt_3d: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices_3d).unwrap(); + /// let dt_3d: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices_3d)?; /// let hull_3d = - /// ConvexHull::from_triangulation(dt_3d.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt_3d.as_triangulation())?; /// assert_eq!(hull_3d.dimension(), 3); + /// # Ok(()) + /// # } /// ``` #[must_use] pub const fn dimension(&self) -> usize { @@ -556,20 +670,34 @@ where /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// let mut dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), - /// ]).unwrap(); + /// ])?; /// /// // Create hull and verify it's valid - /// let hull = ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// let hull = ConvexHull::from_triangulation(dt.as_triangulation())?; /// assert!(hull.is_valid_for_triangulation(dt.as_triangulation())); /// /// // After any modification to the triangulation, the hull would become invalid /// // Note: Currently, hull extension is not yet implemented, so inserting /// // outside points will cause an error. This demonstrates the validation concept. + /// # Ok(()) + /// # } /// ``` #[must_use] pub fn is_valid_for_triangulation(&self, tri: &Triangulation) -> bool { @@ -635,6 +763,18 @@ where /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Create a 3D tetrahedron /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -642,15 +782,17 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// let hull = - /// ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt.as_triangulation())?; /// /// // Manually invalidate the cache (note: takes &self, not &mut self) /// hull.invalidate_cache(); /// /// // The next visibility test will rebuild the cache /// // ... perform visibility operations ... + /// # Ok(()) + /// # } /// ``` pub fn invalidate_cache(&self) { // Clear the cache using ArcSwapOption::store(None) @@ -697,6 +839,18 @@ where /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // 3D example /// let vertices_3d: Vec<_> = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -704,9 +858,9 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt_3d: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices_3d).unwrap(); + /// let dt_3d: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices_3d)?; /// let hull_3d: ConvexHull<_, (), (), 3> = - /// ConvexHull::from_triangulation(dt_3d.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt_3d.as_triangulation())?; /// assert_eq!(hull_3d.number_of_facets(), 4); // Tetrahedron has 4 faces /// /// // 4D example @@ -717,10 +871,12 @@ where /// vertex!([0.0, 0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 0.0, 1.0]), /// ]; - /// let dt_4d: DelaunayTriangulation<_, (), (), 4> = DelaunayTriangulation::new(&vertices_4d).unwrap(); + /// let dt_4d: DelaunayTriangulation<_, (), (), 4> = DelaunayTriangulation::new(&vertices_4d)?; /// let hull_4d = - /// ConvexHull::from_triangulation(dt_4d.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt_4d.as_triangulation())?; /// assert_eq!(hull_4d.number_of_facets(), 5); // 4-simplex has 5 facets + /// # Ok(()) + /// # } /// ``` pub fn from_triangulation( tri: &Triangulation, @@ -825,6 +981,18 @@ where /// use delaunay::prelude::geometry::Coordinate; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Create a 3D tetrahedron /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -832,25 +1000,30 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// let hull = - /// ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt.as_triangulation())?; /// /// // Get a hull facet to test - /// let facet = hull.facet(0).unwrap(); + /// let Some(facet) = hull.facet(0) else { + /// return Err(ExampleError::MissingFacet); + /// }; /// /// // Test visibility from different points /// let inside_point = Point::new([0.2, 0.2, 0.2]); // Inside the tetrahedron /// let outside_point = Point::new([2.0, 2.0, 2.0]); // Outside the tetrahedron /// /// // Inside point should not see the facet (facet not visible) - /// let inside_visible = hull.is_facet_visible_from_point(facet, &inside_point, dt.as_triangulation()).unwrap(); + /// let inside_visible = hull.is_facet_visible_from_point(facet, &inside_point, dt.as_triangulation())?; /// assert!(!inside_visible, "Inside point should not see hull facet"); /// /// // Outside point may see the facet depending on which facet we're testing - /// let outside_visible = hull.is_facet_visible_from_point(facet, &outside_point, dt.as_triangulation()).unwrap(); + /// let outside_visible = hull.is_facet_visible_from_point(facet, &outside_point, dt.as_triangulation())?; /// // Note: The result depends on which facet is selected and the point's position /// // This test just verifies the method executes without error + /// # let _ = outside_visible; + /// # Ok(()) + /// # } /// ``` pub fn is_facet_visible_from_point( &self, @@ -1213,6 +1386,18 @@ where /// use delaunay::prelude::geometry::Coordinate; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Create a 3D tetrahedron /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -1220,19 +1405,21 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// let hull = - /// ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt.as_triangulation())?; /// /// // Test with a point outside the hull /// let outside_point = Point::new([2.0, 2.0, 2.0]); - /// let visible_facets = hull.find_visible_facets(&outside_point, dt.as_triangulation()).unwrap(); + /// let visible_facets = hull.find_visible_facets(&outside_point, dt.as_triangulation())?; /// assert!(!visible_facets.is_empty(), "Outside point should see some facets"); /// /// // Test with a point inside the hull /// let inside_point = Point::new([0.2, 0.2, 0.2]); - /// let visible_facets = hull.find_visible_facets(&inside_point, dt.as_triangulation()).unwrap(); + /// let visible_facets = hull.find_visible_facets(&inside_point, dt.as_triangulation())?; /// assert!(visible_facets.is_empty(), "Inside point should see no facets"); + /// # Ok(()) + /// # } /// ``` pub fn find_visible_facets( &self, @@ -1289,6 +1476,18 @@ where /// use delaunay::prelude::geometry::Coordinate; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Create a 3D tetrahedron /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -1296,19 +1495,21 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// let hull = - /// ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt.as_triangulation())?; /// /// // Test with a point outside the hull - should find a nearest visible facet /// let outside_point = Point::new([2.0, 2.0, 2.0]); - /// let nearest_facet = hull.find_nearest_visible_facet(&outside_point, dt.as_triangulation()).unwrap(); + /// let nearest_facet = hull.find_nearest_visible_facet(&outside_point, dt.as_triangulation())?; /// assert!(nearest_facet.is_some(), "Outside point should have a nearest visible facet"); /// /// // Test with a point inside the hull - should find no visible facets /// let inside_point = Point::new([0.2, 0.2, 0.2]); - /// let nearest_facet = hull.find_nearest_visible_facet(&inside_point, dt.as_triangulation()).unwrap(); + /// let nearest_facet = hull.find_nearest_visible_facet(&inside_point, dt.as_triangulation())?; /// assert!(nearest_facet.is_none(), "Inside point should have no visible facets"); + /// # Ok(()) + /// # } /// ``` pub fn find_nearest_visible_facet( &self, @@ -1411,6 +1612,18 @@ where /// use delaunay::prelude::geometry::Coordinate; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Create a 3D tetrahedron /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -1418,17 +1631,19 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// let hull = - /// ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt.as_triangulation())?; /// /// // Test with a point inside the hull /// let inside_point = Point::new([0.2, 0.2, 0.2]); - /// assert!(!hull.is_point_outside(&inside_point, dt.as_triangulation()).unwrap()); + /// assert!(!hull.is_point_outside(&inside_point, dt.as_triangulation())?); /// /// // Test with a point outside the hull /// let outside_point = Point::new([2.0, 2.0, 2.0]); - /// assert!(hull.is_point_outside(&outside_point, dt.as_triangulation()).unwrap()); + /// assert!(hull.is_point_outside(&outside_point, dt.as_triangulation())?); + /// # Ok(()) + /// # } /// ``` pub fn is_point_outside( &self, @@ -1455,6 +1670,18 @@ where /// use delaunay::prelude::query::ConvexHull; /// use delaunay::vertex; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # #[error(transparent)] + /// # Hull(#[from] delaunay::prelude::query::ConvexHullConstructionError), + /// # #[error(transparent)] + /// # Insertion(#[from] delaunay::prelude::triangulation::InsertionError), + /// # #[error("expected tetrahedron hull to have a first facet")] + /// # MissingFacet, + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // Create a valid 3D tetrahedron /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), @@ -1462,9 +1689,9 @@ where /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; - /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices).unwrap(); + /// let dt: DelaunayTriangulation<_, (), (), 3> = DelaunayTriangulation::new(&vertices)?; /// let hull = - /// ConvexHull::from_triangulation(dt.as_triangulation()).unwrap(); + /// ConvexHull::from_triangulation(dt.as_triangulation())?; /// /// // Validation should pass for a well-formed hull /// assert!(hull.validate(dt.as_triangulation()).is_ok()); @@ -1473,6 +1700,8 @@ where /// let empty_hull: ConvexHull, (), (), 3> = ConvexHull::default(); /// // Note: validate() requires a TDS, so use an empty TDS for validation /// assert!(empty_hull.validate(dt.as_triangulation()).is_ok()); + /// # Ok(()) + /// # } /// ``` pub fn validate( &self, diff --git a/src/lib.rs b/src/lib.rs index ce1b4d71..6300eed1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,13 +71,14 @@ //! ```rust //! use delaunay::prelude::triangulation::*; //! +//! # fn main() -> Result<(), DelaunayTriangulationConstructionError> { //! let vertices = vec![ //! vertex!([0.0, 0.0, 0.0]), //! vertex!([1.0, 0.0, 0.0]), //! vertex!([0.0, 1.0, 0.0]), //! vertex!([0.0, 0.0, 1.0]), //! ]; -//! let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); +//! let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! //! // Levels 1–2: elements + structural (TDS) //! assert!(dt.tds().validate().is_ok()); @@ -90,6 +91,8 @@ //! //! // Levels 1–4: full cumulative validation //! assert!(dt.validate().is_ok()); +//! # Ok(()) +//! # } //! ``` //! //! ### Topology guarantees and insertion-time validation (`TopologyGuarantee`, `ValidationPolicy`) @@ -97,13 +100,14 @@ //! ```rust //! use delaunay::prelude::triangulation::*; //! +//! # fn main() -> Result<(), DelaunayTriangulationConstructionError> { //! let vertices = vec![ //! vertex!([0.0, 0.0, 0.0]), //! vertex!([1.0, 0.0, 0.0]), //! vertex!([0.0, 1.0, 0.0]), //! vertex!([0.0, 0.0, 1.0]), //! ]; -//! let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); +//! let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! //! assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); //! assert_eq!(dt.validation_policy(), ValidationPolicy::OnSuspicion); @@ -113,6 +117,8 @@ //! //! assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); //! assert_eq!(dt.validation_policy(), ValidationPolicy::Always); +//! # Ok(()) +//! # } //! ``` //! //! ### Transactional operations and duplicate rejection @@ -120,12 +126,13 @@ //! ```rust //! use delaunay::prelude::triangulation::*; //! +//! # fn main() -> Result<(), DelaunayTriangulationConstructionError> { //! let vertices = vec![ //! vertex!([0.0, 0.0]), //! vertex!([1.0, 0.0]), //! vertex!([0.0, 1.0]), //! ]; -//! let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); +//! let mut dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! //! let before_vertices = dt.number_of_vertices(); //! let before_cells = dt.number_of_cells(); @@ -137,6 +144,8 @@ //! // On error, the triangulation is unchanged. //! assert_eq!(dt.number_of_vertices(), before_vertices); //! assert_eq!(dt.number_of_cells(), before_cells); +//! # Ok(()) +//! # } //! ``` //! //! # Triangulation invariants and validation hierarchy @@ -219,22 +228,32 @@ //! ```rust //! use delaunay::prelude::triangulation::*; //! +//! # #[derive(Debug, thiserror::Error)] +//! # enum ExampleError { +//! # #[error(transparent)] +//! # Construction(#[from] DelaunayTriangulationConstructionError), +//! # #[error(transparent)] +//! # Insertion(#[from] InsertionError), +//! # } +//! # fn main() -> Result<(), ExampleError> { //! let vertices = vec![ //! vertex!([0.0, 0.0, 0.0]), //! vertex!([1.0, 0.0, 0.0]), //! vertex!([0.0, 1.0, 0.0]), //! vertex!([0.0, 0.0, 1.0]), //! ]; -//! let mut dt = DelaunayTriangulation::new(&vertices).unwrap(); +//! let mut dt = DelaunayTriangulation::new(&vertices)?; //! //! // Performance mode: disable insertion-time Level 3 topology validation. //! dt.set_validation_policy(ValidationPolicy::Never); //! //! // Do incremental work... -//! dt.insert(vertex!([0.2, 0.2, 0.2])).unwrap(); +//! dt.insert(vertex!([0.2, 0.2, 0.2]))?; //! //! // ...then explicitly validate the topology layer when you need a certificate. //! assert!(dt.as_triangulation().validate().is_ok()); +//! # Ok(()) +//! # } //! ``` //! //! ### Choosing Level 3 topology guarantee (`TopologyGuarantee`) @@ -263,33 +282,39 @@ //! ```rust //! use delaunay::prelude::triangulation::*; //! +//! # fn main() -> Result<(), DelaunayTriangulationConstructionError> { //! let vertices = vec![ //! vertex!([0.0, 0.0, 0.0]), //! vertex!([1.0, 0.0, 0.0]), //! vertex!([0.0, 1.0, 0.0]), //! vertex!([0.0, 0.0, 1.0]), //! ]; -//! let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); +//! let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! //! // For `TopologyGuarantee::PLManifold`, full certification includes a completion-time //! // vertex-link validation pass. //! assert!(dt.as_triangulation().validate_at_completion().is_ok()); +//! # Ok(()) +//! # } //! ``` //! //! ```rust //! use delaunay::prelude::triangulation::*; //! +//! # fn main() -> Result<(), DelaunayTriangulationConstructionError> { //! let vertices = vec![ //! vertex!([0.0, 0.0, 0.0]), //! vertex!([1.0, 0.0, 0.0]), //! vertex!([0.0, 1.0, 0.0]), //! vertex!([0.0, 0.0, 1.0]), //! ]; -//! let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); +//! let dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; //! //! // `validate()` returns the first violation; `validation_report()` is intended for //! // debugging/telemetry where you want the full set of violated invariants. //! assert!(dt.validation_report().is_ok()); +//! # Ok(()) +//! # } //! ``` //! //! For implementation details on invariant enforcement, see [`core::algorithms::incremental_insertion`]. @@ -800,17 +825,27 @@ pub mod triangulation { /// use delaunay::prelude::triangulation::*; /// use delaunay::prelude::topology::validation; /// +/// # #[derive(Debug, thiserror::Error)] +/// # enum ExampleError { +/// # #[error(transparent)] +/// # Construction(#[from] DelaunayTriangulationConstructionError), +/// # #[error(transparent)] +/// # Topology(#[from] delaunay::topology::TopologyError), +/// # } +/// # fn main() -> Result<(), ExampleError> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), /// vertex!([0.0, 1.0, 0.0]), /// vertex!([0.0, 0.0, 1.0]), /// ]; -/// let dt = DelaunayTriangulation::new(&vertices).unwrap(); +/// let dt = DelaunayTriangulation::new(&vertices)?; /// -/// let result = validation::validate_triangulation_euler(dt.tds()).unwrap(); +/// let result = validation::validate_triangulation_euler(dt.tds())?; /// assert_eq!(result.chi, 1); // Tetrahedron has χ = 1 /// assert!(result.is_valid()); +/// # Ok(()) +/// # } /// ``` pub mod topology { /// Traits for topological spaces and error types @@ -1149,13 +1184,18 @@ pub mod prelude { /// # Examples /// /// ```rust - /// use delaunay::prelude::generators::generate_random_points_seeded; + /// use delaunay::prelude::generators::{ + /// RandomPointGenerationError, generate_random_points_seeded, + /// }; /// use delaunay::prelude::geometry::Point; /// + /// # fn main() -> Result<(), RandomPointGenerationError> { /// let points: Vec> = - /// generate_random_points_seeded(4, (0.0, 1.0), 42).unwrap(); + /// generate_random_points_seeded(4, (0.0, 1.0), 42)?; /// /// assert_eq!(points.len(), 4); + /// # Ok(()) + /// # } /// ``` pub mod generators { pub use crate::core::triangulation::TopologyGuarantee; diff --git a/src/triangulation/builder.rs b/src/triangulation/builder.rs index 7ad469c5..1db0627e 100644 --- a/src/triangulation/builder.rs +++ b/src/triangulation/builder.rs @@ -35,6 +35,7 @@ //! use delaunay::prelude::triangulation::DelaunayTriangulationBuilder; //! use delaunay::vertex; //! +//! # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { //! let vertices = vec![ //! vertex!([0.0, 0.0]), //! vertex!([1.0, 0.0]), @@ -42,10 +43,11 @@ //! ]; //! //! let dt = DelaunayTriangulationBuilder::new(&vertices) -//! .build::<()>() -//! .unwrap(); +//! .build::<()>()?; //! //! assert_eq!(dt.number_of_vertices(), 3); +//! # Ok(()) +//! # } //! ``` //! //! ## Toroidal construction (Phase 1: canonicalization only) @@ -54,6 +56,7 @@ //! use delaunay::prelude::triangulation::DelaunayTriangulationBuilder; //! use delaunay::vertex; //! +//! # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { //! // Vertices that fall outside [0, 1)² are wrapped before triangulation. //! let vertices = vec![ //! vertex!([0.2, 0.3]), @@ -64,10 +67,11 @@ //! //! let dt = DelaunayTriangulationBuilder::new(&vertices) //! .toroidal([1.0, 1.0]) -//! .build::<()>() -//! .unwrap(); +//! .build::<()>()?; //! //! assert_eq!(dt.number_of_vertices(), 4); +//! # Ok(()) +//! # } //! ``` //! //! ## Toroidal construction (Phase 2: full periodic / image-point method) @@ -80,6 +84,7 @@ //! use delaunay::prelude::triangulation::DelaunayTriangulationBuilder; //! use delaunay::vertex; //! +//! # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { //! let vertices = vec![ //! vertex!([0.1, 0.2]), //! vertex!([0.4, 0.7]), @@ -93,12 +98,13 @@ //! let kernel = RobustKernel::new(); //! let dt = DelaunayTriangulationBuilder::new(&vertices) //! .toroidal_periodic([1.0, 1.0]) -//! .build_with_kernel::<_, ()>(&kernel) -//! .unwrap(); +//! .build_with_kernel::<_, ()>(&kernel)?; //! //! assert_eq!(dt.number_of_vertices(), 7); //! // Every vertex has a valid incident cell (no boundary). //! assert!(dt.tds().is_valid().is_ok()); +//! # Ok(()) +//! # } //! ``` #![forbid(unsafe_code)] @@ -307,15 +313,11 @@ fn search_closed_2d_selection( /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; /// let cells = vec![vec![0, 1]]; // Wrong arity for 2D (needs 3 vertices) /// -/// let err = DelaunayTriangulationBuilder::from_vertices_and_cells(&vertices, &cells) -/// .build::<()>() -/// .unwrap_err(); -/// /// assert!(matches!( -/// err, -/// DelaunayTriangulationConstructionError::ExplicitConstruction( -/// ExplicitConstructionError::InvalidCellArity { cell_index: 0, actual: 2, expected: 3 } -/// ) +/// DelaunayTriangulationBuilder::from_vertices_and_cells(&vertices, &cells).build::<()>(), +/// Err(DelaunayTriangulationConstructionError::ExplicitConstruction( +/// ExplicitConstructionError::InvalidCellArity { cell_index: 0, actual: 2, expected: 3 }, +/// )) /// )); /// ``` #[derive(Clone, Debug, Error, PartialEq, Eq)] @@ -405,6 +407,7 @@ pub enum ExplicitConstructionError { /// }; /// use delaunay::vertex; /// +/// # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -415,10 +418,11 @@ pub enum ExplicitConstructionError { /// let dt = DelaunayTriangulationBuilder::new(&vertices) /// .topology_guarantee(TopologyGuarantee::Pseudomanifold) /// .construction_options(ConstructionOptions::default()) -/// .build::<()>() -/// .unwrap(); +/// .build::<()>()?; /// /// assert_eq!(dt.number_of_vertices(), 4); +/// # Ok(()) +/// # } /// ``` pub struct DelaunayTriangulationBuilder<'v, T, U, const D: usize> { vertices: &'v [Vertex], @@ -478,9 +482,10 @@ where /// ```rust /// use delaunay::prelude::triangulation::*; /// + /// # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { /// // No vertex data (U = () inferred) /// let vertices = vec![vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0])]; - /// let _dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>().unwrap(); + /// let _dt = DelaunayTriangulationBuilder::new(&vertices).build::<()>()?; /// /// // Typed vertex data (U = i32 inferred) /// let typed: [Vertex; 3] = [ @@ -488,7 +493,9 @@ where /// vertex!([1.0, 0.0], 2), /// vertex!([0.0, 1.0], 3), /// ]; - /// let _dt = DelaunayTriangulationBuilder::new(&typed).build::<()>().unwrap(); + /// let _dt = DelaunayTriangulationBuilder::new(&typed).build::<()>()?; + /// # Ok(()) + /// # } /// ``` #[must_use] pub fn new(vertices: &'v [Vertex]) -> Self { @@ -528,6 +535,7 @@ where /// }; /// use delaunay::vertex; /// + /// # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0]), /// vertex!([1.0, 0.0]), @@ -537,22 +545,20 @@ where /// let cells = vec![vec![0, 1, 2], vec![0, 2, 3]]; /// /// let dt = DelaunayTriangulationBuilder::from_vertices_and_cells(&vertices, &cells) - /// .build::<()>() - /// .unwrap(); + /// .build::<()>()?; /// /// assert_eq!(dt.number_of_vertices(), 4); /// assert_eq!(dt.number_of_cells(), 2); /// /// let bad_cells = vec![vec![0, 1]]; // Wrong arity for a 2D simplex. - /// let err = DelaunayTriangulationBuilder::from_vertices_and_cells(&vertices, &bad_cells) - /// .build::<()>() - /// .unwrap_err(); /// assert!(matches!( - /// err, - /// DelaunayTriangulationConstructionError::ExplicitConstruction( - /// ExplicitConstructionError::InvalidCellArity { .. } - /// ) + /// DelaunayTriangulationBuilder::from_vertices_and_cells(&vertices, &bad_cells).build::<()>(), + /// Err(DelaunayTriangulationConstructionError::ExplicitConstruction( + /// ExplicitConstructionError::InvalidCellArity { .. }, + /// )) /// )); + /// # Ok(()) + /// # } /// ``` #[must_use] pub fn from_vertices_and_cells( @@ -594,19 +600,28 @@ where /// use delaunay::prelude::geometry::{Coordinate, Point}; /// use delaunay::prelude::triangulation::{DelaunayTriangulationBuilder, Vertex, VertexBuilder}; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Vertex(#[from] delaunay::prelude::triangulation::VertexBuilderError), + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { /// let vertices: Vec> = vec![ - /// VertexBuilder::default().point(Point::new([0.0_f32, 0.0])).build().unwrap(), - /// VertexBuilder::default().point(Point::new([1.0_f32, 0.0])).build().unwrap(), - /// VertexBuilder::default().point(Point::new([0.0_f32, 1.0])).build().unwrap(), + /// VertexBuilder::default().point(Point::new([0.0_f32, 0.0])).build()?, + /// VertexBuilder::default().point(Point::new([1.0_f32, 0.0])).build()?, + /// VertexBuilder::default().point(Point::new([0.0_f32, 1.0])).build()?, /// ]; /// let cells = vec![vec![0, 1, 2]]; /// /// let dt = DelaunayTriangulationBuilder::from_vertices_and_cells_generic(&vertices, &cells) - /// .build::<()>() - /// .unwrap(); + /// .build::<()>()?; /// /// assert_eq!(dt.number_of_vertices(), 3); /// assert_eq!(dt.number_of_cells(), 1); + /// # Ok(()) + /// # } /// ``` #[must_use] pub fn from_vertices_and_cells_generic( @@ -636,18 +651,27 @@ where /// use delaunay::prelude::geometry::{Coordinate, Point}; /// use delaunay::prelude::triangulation::{DelaunayTriangulationBuilder, Vertex, VertexBuilder}; /// + /// # #[derive(Debug, thiserror::Error)] + /// # enum ExampleError { + /// # #[error(transparent)] + /// # Vertex(#[from] delaunay::prelude::triangulation::VertexBuilderError), + /// # #[error(transparent)] + /// # Construction(#[from] delaunay::prelude::triangulation::DelaunayTriangulationConstructionError), + /// # } + /// # fn main() -> Result<(), ExampleError> { /// // f32 vertices — new() is f64-only, so from_vertices is required here. /// let vertices: Vec> = vec![ - /// VertexBuilder::default().point(Point::new([0.0_f32, 0.0])).build().unwrap(), - /// VertexBuilder::default().point(Point::new([1.0_f32, 0.0])).build().unwrap(), - /// VertexBuilder::default().point(Point::new([0.0_f32, 1.0])).build().unwrap(), + /// VertexBuilder::default().point(Point::new([0.0_f32, 0.0])).build()?, + /// VertexBuilder::default().point(Point::new([1.0_f32, 0.0])).build()?, + /// VertexBuilder::default().point(Point::new([0.0_f32, 1.0])).build()?, /// ]; /// /// let dt = DelaunayTriangulationBuilder::from_vertices(&vertices) - /// .build::<()>() - /// .unwrap(); + /// .build::<()>()?; /// /// assert_eq!(dt.number_of_vertices(), 3); + /// # Ok(()) + /// # } /// ``` #[must_use] pub fn from_vertices(vertices: &'v [Vertex]) -> Self { @@ -680,6 +704,7 @@ where /// use delaunay::prelude::triangulation::DelaunayTriangulationBuilder; /// use delaunay::vertex; /// + /// # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.2, 0.3]), /// vertex!([0.8, 0.1]), @@ -689,10 +714,11 @@ where /// /// let dt = DelaunayTriangulationBuilder::new(&vertices) /// .toroidal([1.0, 1.0]) - /// .build::<()>() - /// .unwrap(); + /// .build::<()>()?; /// /// assert_eq!(dt.number_of_vertices(), 4); + /// # Ok(()) + /// # } /// ``` #[must_use] pub const fn toroidal(mut self, domain: [f64; D]) -> Self { @@ -728,6 +754,7 @@ where /// use delaunay::prelude::triangulation::DelaunayTriangulationBuilder; /// use delaunay::vertex; /// + /// # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.1, 0.2]), /// vertex!([0.4, 0.7]), @@ -741,11 +768,12 @@ where /// let kernel = RobustKernel::new(); /// let dt = DelaunayTriangulationBuilder::new(&vertices) /// .toroidal_periodic([1.0, 1.0]) - /// .build_with_kernel::<_, ()>(&kernel) - /// .unwrap(); + /// .build_with_kernel::<_, ()>(&kernel)?; /// /// assert_eq!(dt.number_of_vertices(), 7); /// assert!(dt.tds().is_valid().is_ok()); + /// # Ok(()) + /// # } /// ``` #[must_use] pub const fn toroidal_periodic(mut self, domain: [f64; D]) -> Self { @@ -764,6 +792,7 @@ where /// use delaunay::prelude::triangulation::{DelaunayTriangulationBuilder, TopologyGuarantee}; /// use delaunay::vertex; /// + /// # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0]), /// vertex!([1.0, 0.0]), @@ -772,10 +801,11 @@ where /// /// let dt = DelaunayTriangulationBuilder::new(&vertices) /// .topology_guarantee(TopologyGuarantee::Pseudomanifold) - /// .build::<()>() - /// .unwrap(); + /// .build::<()>()?; /// /// assert_eq!(dt.topology_guarantee(), TopologyGuarantee::Pseudomanifold); + /// # Ok(()) + /// # } /// ``` #[must_use] pub const fn topology_guarantee(mut self, topology_guarantee: TopologyGuarantee) -> Self { @@ -841,6 +871,7 @@ where /// }; /// use delaunay::vertex; /// + /// # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0]), /// vertex!([1.0, 0.0]), @@ -852,10 +883,11 @@ where /// /// let dt = DelaunayTriangulationBuilder::new(&vertices) /// .construction_options(opts) - /// .build::<()>() - /// .unwrap(); + /// .build::<()>()?; /// /// assert_eq!(dt.number_of_vertices(), 3); + /// # Ok(()) + /// # } /// ``` #[must_use] pub const fn construction_options(mut self, construction_options: ConstructionOptions) -> Self { @@ -1025,6 +1057,7 @@ where /// use delaunay::prelude::triangulation::DelaunayTriangulationBuilder; /// use delaunay::vertex; /// + /// # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -1033,11 +1066,12 @@ where /// ]; /// /// let dt = DelaunayTriangulationBuilder::new(&vertices) - /// .build::<()>() - /// .unwrap(); + /// .build::<()>()?; /// /// assert_eq!(dt.number_of_vertices(), 4); /// assert!(dt.validate().is_ok()); + /// # Ok(()) + /// # } /// ``` pub fn build( self, @@ -1076,6 +1110,7 @@ where /// use delaunay::prelude::triangulation::DelaunayTriangulationBuilder; /// use delaunay::vertex; /// + /// # fn main() -> Result<(), delaunay::prelude::triangulation::DelaunayTriangulationConstructionError> { /// let vertices = vec![ /// vertex!([0.0, 0.0, 0.0]), /// vertex!([1.0, 0.0, 0.0]), @@ -1085,10 +1120,11 @@ where /// /// let kernel = RobustKernel::new(); /// let dt = DelaunayTriangulationBuilder::new(&vertices) - /// .build_with_kernel::<_, ()>(&kernel) - /// .unwrap(); + /// .build_with_kernel::<_, ()>(&kernel)?; /// /// assert_eq!(dt.number_of_vertices(), 4); + /// # Ok(()) + /// # } /// ``` pub fn build_with_kernel( self, diff --git a/src/triangulation/delaunay.rs b/src/triangulation/delaunay.rs index 3a98cc38..f3f53576 100644 --- a/src/triangulation/delaunay.rs +++ b/src/triangulation/delaunay.rs @@ -2948,8 +2948,11 @@ where err, DelaunayTriangulationConstructionError::Triangulation( DelaunayConstructionFailure::Tds { - reason: TdsConstructionFailure::DuplicateUuid { .. }, + reason: TdsConstructionFailure::DuplicateUuid { .. } + | TdsConstructionFailure::Validation { .. }, } | DelaunayConstructionFailure::InternalInconsistency { .. } + | DelaunayConstructionFailure::InsertionTopologyValidation { .. } + | DelaunayConstructionFailure::FinalTopologyValidation { .. }, ) ) } @@ -3396,31 +3399,6 @@ where use_global_repair_fallback, )?; - // Final validation at construction completion for PLManifold/PLManifoldStrict. - // This ensures PL-manifold guarantee even with ValidationPolicy::OnSuspicion during - // incremental insertion. - if dt - .tri - .topology_guarantee - .requires_vertex_links_at_completion() - { - tracing::debug!("post-construction: starting topology validation (build)"); - let validation_started = Instant::now(); - let validation_result = dt.tri.validate(); - tracing::debug!( - elapsed = ?validation_started.elapsed(), - success = validation_result.is_ok(), - "post-construction: topology validation (build) completed" - ); - if let Err(err) = validation_result { - return Err(TriangulationConstructionError::FinalTopologyValidation { - message: "PL-manifold validation failed after construction".to_string(), - source: Box::new(err), - } - .into()); - } - } - // `DelaunayCheckPolicy::EndOnly`: always run a final global Delaunay validation pass after // batch construction. tracing::debug!("post-construction: starting Delaunay validation (build)"); @@ -3462,34 +3440,6 @@ where use_global_repair_fallback, )?; - // Final validation at construction completion for PLManifold/PLManifoldStrict. - // This ensures PL-manifold guarantee even with ValidationPolicy::OnSuspicion during - // incremental insertion. - if dt - .tri - .topology_guarantee - .requires_vertex_links_at_completion() - { - tracing::debug!("post-construction: starting topology validation (build stats)"); - let validation_started = Instant::now(); - let validation_result = dt.tri.validate(); - tracing::debug!( - elapsed = ?validation_started.elapsed(), - success = validation_result.is_ok(), - "post-construction: topology validation (build stats) completed" - ); - if let Err(err) = validation_result { - return Err(DelaunayTriangulationConstructionErrorWithStatistics { - error: TriangulationConstructionError::FinalTopologyValidation { - message: "PL-manifold validation failed after construction".to_string(), - source: Box::new(err), - } - .into(), - statistics: stats, - }); - } - } - // `DelaunayCheckPolicy::EndOnly`: always run a final global Delaunay validation pass after // batch construction. tracing::debug!("post-construction: starting Delaunay validation (build stats)"); @@ -11517,6 +11467,60 @@ mod tests { ); } + #[test] + fn test_is_non_retryable_construction_error_tds_validation() { + let err: DelaunayTriangulationConstructionError = TriangulationConstructionError::Tds( + TdsConstructionError::ValidationError(TdsError::InconsistentDataStructure { + message: "test".to_string(), + }), + ) + .into(); + assert!( + DelaunayTriangulation::, (), (), 3>::is_non_retryable_construction_error( + &err + ), + "TDS validation failures should be non-retryable" + ); + } + + #[test] + fn test_is_non_retryable_construction_error_topology_validation_buckets() { + let vertex_key = VertexKey::from(KeyData::from_ffi(1)); + let insertion_err: DelaunayTriangulationConstructionError = + TriangulationConstructionError::InsertionTopologyValidation { + message: "test".to_string(), + source: TriangulationValidationError::IsolatedVertex { + vertex_key, + vertex_uuid: Uuid::nil(), + }, + } + .into(); + let final_err: DelaunayTriangulationConstructionError = + TriangulationConstructionError::FinalTopologyValidation { + message: "test".to_string(), + source: Box::new(InvariantError::Triangulation( + TriangulationValidationError::IsolatedVertex { + vertex_key, + vertex_uuid: Uuid::nil(), + }, + )), + } + .into(); + + assert!( + DelaunayTriangulation::, (), (), 3>::is_non_retryable_construction_error( + &insertion_err + ), + "InsertionTopologyValidation should be non-retryable" + ); + assert!( + DelaunayTriangulation::, (), (), 3>::is_non_retryable_construction_error( + &final_err + ), + "FinalTopologyValidation should be non-retryable" + ); + } + #[test] fn test_is_non_retryable_construction_error_false_for_geometric_degeneracy() { let err: DelaunayTriangulationConstructionError = diff --git a/tests/allocation_api.rs b/tests/allocation_api.rs index c995feac..8d394da5 100644 --- a/tests/allocation_api.rs +++ b/tests/allocation_api.rs @@ -29,9 +29,6 @@ pub mod test_helpers { /// Helper to measure allocations with error handling /// - /// # Panics - /// - /// Panics if the closure `f` does not complete successfully. #[cfg(feature = "count-allocations")] pub fn measure_with_result(f: F) -> (R, allocation_counter::AllocationInfo) where @@ -42,7 +39,10 @@ pub mod test_helpers { result = Some(f()); }); println!("Memory info: {info:?}"); - (result.expect("Closure should have set result"), info) + let Some(result) = result else { + unreachable!("allocation_counter::measure did not execute the closure"); + }; + (result, info) } /// Fallback for when allocation counting is disabled @@ -92,12 +92,13 @@ pub mod test_helpers { /// Create a triangulation with some test vertices /// - /// # Panics + /// # Errors /// - /// Panics if triangulation creation with vertices fails. - #[must_use] - pub fn create_test_tds_with_vertices() -> DelaunayTriangulation, (), (), 3> - { + /// Returns the typed construction error if triangulation creation with vertices fails. + pub fn create_test_tds_with_vertices() -> Result< + DelaunayTriangulation, (), (), 3>, + DelaunayTriangulationConstructionError, + > { let vertices = vec![ vertex!([0.0, 0.0, 0.0]), vertex!([1.0, 0.0, 0.0]), @@ -105,7 +106,6 @@ pub mod test_helpers { vertex!([0.0, 0.0, 1.0]), ]; DelaunayTriangulation::new_with_topology_guarantee(&vertices, TopologyGuarantee::PLManifold) - .expect("Failed to create triangulation with vertices") } /// Print memory allocation summary diff --git a/tests/delaunay_public_api_coverage.rs b/tests/delaunay_public_api_coverage.rs index d31573b0..54fb1f43 100644 --- a/tests/delaunay_public_api_coverage.rs +++ b/tests/delaunay_public_api_coverage.rs @@ -1,11 +1,16 @@ //! Public API integration coverage for `DelaunayTriangulation`. #![forbid(unsafe_code)] +#![expect( + clippy::result_large_err, + reason = "tests preserve typed construction and insertion errors" +)] use delaunay::prelude::geometry::AdaptiveKernel; use delaunay::prelude::triangulation::{ ConstructionOptions, DedupPolicy, DelaunayTriangulation, - DelaunayTriangulationConstructionError, InsertionOrderStrategy, RetryPolicy, TopologyGuarantee, + DelaunayTriangulationConstructionError, InsertionError, InsertionOrderStrategy, RetryPolicy, + TopologyGuarantee, }; use delaunay::vertex; #[cfg(feature = "diagnostics")] @@ -13,8 +18,22 @@ use rand::{RngExt, SeedableRng, rngs::StdRng}; type Dt = DelaunayTriangulation, (), (), D>; +#[derive(Debug, thiserror::Error)] +enum PublicApiCoverageTestError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + Insertion(#[from] InsertionError), + #[cfg(feature = "diagnostics")] + #[error("stale VertexKey returned from insert() after heuristic rebuild")] + StaleVertexKey, + #[cfg(feature = "diagnostics")] + #[error("no stale-key case found after {cases} attempts")] + NoStaleKeyCase { cases: usize }, +} + #[test] -fn topology_options_smoke_3d() { +fn topology_options_smoke_3d() -> Result<(), PublicApiCoverageTestError> { let vertices = vec![ vertex!([0.0, 0.0, 0.0]), vertex!([1.0, 0.0, 0.0]), @@ -31,12 +50,12 @@ fn topology_options_smoke_3d() { &vertices, TopologyGuarantee::PLManifold, options, - ) - .expect("3D construction with explicit options should succeed"); + )?; assert_eq!(dt.number_of_vertices(), 4); assert_eq!(dt.number_of_cells(), 1); assert!(dt.validate().is_ok()); + Ok(()) } #[test] @@ -71,20 +90,19 @@ fn statistics_default_on_preprocess_error() { #[test] #[allow(deprecated)] -fn as_triangulation_mut_valid_view() { +fn as_triangulation_mut_valid_view() -> Result<(), PublicApiCoverageTestError> { let vertices = vec![ vertex!([0.0, 0.0]), vertex!([1.0, 0.0]), vertex!([0.0, 1.0]), ]; - let mut dt: DelaunayTriangulation<_, (), (), 2> = - DelaunayTriangulation::new(&vertices).expect("2D construction should succeed"); + let mut dt: DelaunayTriangulation<_, (), (), 2> = DelaunayTriangulation::new(&vertices)?; - dt.insert(vertex!([0.2, 0.2])) - .expect("interior insertion should succeed"); + dt.insert(vertex!([0.2, 0.2]))?; let tri = dt.as_triangulation_mut(); assert!(tri.is_valid().is_ok()); + Ok(()) } /// Slow search helper to find a natural stale-key repro case. @@ -95,7 +113,7 @@ fn as_triangulation_mut_valid_view() { #[cfg(feature = "diagnostics")] #[test] #[ignore = "manual search helper; run explicitly to discover natural repro cases"] -fn find_stale_key_after_rebuild() { +fn find_stale_key_after_rebuild() -> Result<(), PublicApiCoverageTestError> { const DIM: usize = 4; const INITIAL_COUNT: usize = 12; const CASES: usize = 2_000; @@ -153,8 +171,8 @@ fn find_stale_key_after_rebuild() { } tracing::debug!("inserted vertex coords: {inserted_coords:?}"); } - panic!("stale VertexKey returned from insert() after heuristic rebuild"); + return Err(PublicApiCoverageTestError::StaleVertexKey); } - panic!("no stale-key case found after {CASES} attempts"); + Err(PublicApiCoverageTestError::NoStaleKeyCase { cases: CASES }) } diff --git a/tests/prelude_exports.rs b/tests/prelude_exports.rs index affb0400..ed88f16a 100644 --- a/tests/prelude_exports.rs +++ b/tests/prelude_exports.rs @@ -4,6 +4,13 @@ //! paths so doctests, integration tests, examples, and benchmarks have a small //! import contract to copy from. +#![forbid(unsafe_code)] +#![expect( + clippy::result_large_err, + reason = "tests preserve typed construction, repair, and delaunayize errors" +)] + +use delaunay::prelude::DelaunayValidationError; use delaunay::prelude::algorithms::LocateResult; #[cfg(feature = "diagnostics")] use delaunay::prelude::collections::CellKeyBuffer; @@ -12,7 +19,7 @@ use delaunay::prelude::diagnostics::{ DelaunayViolationDetail, DelaunayViolationReport, debug_print_first_delaunay_violation, delaunay_violation_report, verify_conflict_region_completeness, }; -use delaunay::prelude::generators::generate_random_points_seeded; +use delaunay::prelude::generators::{RandomPointGenerationError, generate_random_points_seeded}; #[cfg(feature = "diagnostics")] use delaunay::prelude::geometry::Coordinate; use delaunay::prelude::geometry::{AdaptiveKernel, Point}; @@ -23,6 +30,7 @@ use delaunay::prelude::ordering::{ use delaunay::prelude::query::ConvexHull; #[cfg(feature = "diagnostics")] use delaunay::prelude::tds::Tds; +use delaunay::prelude::tds::TdsMutationError; use delaunay::prelude::triangulation::delaunayize::{ DelaunayizeConfig, DelaunayizeError, DelaunayizeOutcome, delaunayize_by_flips, }; @@ -35,19 +43,33 @@ use delaunay::prelude::triangulation::repair::{ }; use delaunay::prelude::triangulation::{ ConstructionOptions, DelaunayConstructionFailure, DelaunayRepairOperation, - DelaunayTriangulation, DelaunayTriangulationValidationError, InsertionOrderStrategy, Vertex, + DelaunayTriangulation, DelaunayTriangulationConstructionError, + DelaunayTriangulationValidationError, InsertionOrderStrategy, Vertex, }; use delaunay::vertex; +#[derive(Debug, thiserror::Error)] +enum PreludeExportTestError { + #[error(transparent)] + RandomPointGeneration(#[from] RandomPointGenerationError), + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + DelaunayValidation(#[from] DelaunayValidationError), + #[error(transparent)] + DelaunayRepair(#[from] DelaunayRepairError), + #[error(transparent)] + Delaunayize(#[from] DelaunayizeError), +} + /// Proves the focused flips prelude exports the trait bound expected by benchmarks. const fn assert_bistellar_flips(_: &impl BistellarFlips, (), 3>) {} const fn assert_send_sync_unpin() {} #[test] -fn preludes_cover_bench_apis() { - let _generated_points: Vec> = - generate_random_points_seeded(3, (0.0, 1.0), 42).unwrap(); +fn preludes_cover_bench_apis() -> Result<(), PreludeExportTestError> { + let _generated_points: Vec> = generate_random_points_seeded(3, (0.0, 1.0), 42)?; let vertices: Vec> = vec![ vertex!([0.0, 0.0, 0.0]), @@ -57,7 +79,7 @@ fn preludes_cover_bench_apis() { ]; let options = ConstructionOptions::default().with_insertion_order(InsertionOrderStrategy::Input); - let dt = DelaunayTriangulation::new_with_options(&vertices, options).unwrap(); + let dt = DelaunayTriangulation::new_with_options(&vertices, options)?; assert_eq!(dt.topology_guarantee(), TopologyGuarantee::PLManifold); assert!(dt.boundary_facets().count() > 0); @@ -72,17 +94,19 @@ fn preludes_cover_bench_apis() { DelaunayConstructionFailure::GeometricDegeneracy { .. } )); assert!(matches!(LocateResult::Outside, LocateResult::Outside)); + assert_send_sync_unpin::(); + Ok(()) } #[test] -fn diagnostic_preludes_cover_repair_apis() { +fn diagnostic_preludes_cover_repair_apis() -> Result<(), PreludeExportTestError> { let vertices: Vec> = vec![ vertex!([0.0, 0.0, 0.0]), vertex!([1.0, 0.0, 0.0]), vertex!([0.0, 1.0, 0.0]), vertex!([0.0, 0.0, 1.0]), ]; - let mut dt = DelaunayTriangulation::new(&vertices).unwrap(); + let mut dt = DelaunayTriangulation::new(&vertices)?; let repair_stats = DelaunayRepairStats::default(); let repair_outcome = DelaunayRepairOutcome { @@ -122,20 +146,21 @@ fn diagnostic_preludes_cover_repair_apis() { }; assert!(validation_error.to_string().contains("vertex removal")); - verify_delaunay_for_triangulation(dt.as_triangulation()).unwrap(); + verify_delaunay_for_triangulation(dt.as_triangulation())?; - let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default()).unwrap(); + let outcome = delaunayize_by_flips(&mut dt, DelaunayizeConfig::default())?; assert!(!outcome.used_fallback_rebuild); let _typed_outcome: DelaunayizeOutcome = outcome; let _typed_error: Option = None; + Ok(()) } #[cfg(feature = "diagnostics")] #[test] -fn diagnostics_prelude_covers_opt_in_helpers() { +fn diagnostics_prelude_covers_opt_in_helpers() -> Result<(), PreludeExportTestError> { let tds: Tds = Tds::empty(); debug_print_first_delaunay_violation(&tds, None); - let report = delaunay_violation_report(&tds, None).unwrap(); + let report = delaunay_violation_report(&tds, None)?; let _typed_report: DelaunayViolationReport = report; let _typed_detail: Option = None; @@ -146,6 +171,7 @@ fn diagnostics_prelude_covers_opt_in_helpers() { verify_conflict_region_completeness(&tds, &kernel, &point, &conflict_cells), 0 ); + Ok(()) } #[test] diff --git a/tests/public_topology_api.rs b/tests/public_topology_api.rs index d5cbf55b..ead385fe 100644 --- a/tests/public_topology_api.rs +++ b/tests/public_topology_api.rs @@ -8,9 +8,26 @@ use delaunay::prelude::TopologyGuarantee; use delaunay::prelude::query::*; +use delaunay::prelude::triangulation::DelaunayTriangulationConstructionError; + +#[derive(Debug, thiserror::Error)] +enum PublicTopologyApiTestError { + #[error(transparent)] + Construction(#[from] DelaunayTriangulationConstructionError), + #[error(transparent)] + AdjacencyIndex(#[from] AdjacencyIndexBuildError), + #[error("single tetrahedron triangulation has no vertices")] + EmptySingleTetrahedronVertices, + #[error("single tetrahedron triangulation has no cells")] + EmptySingleTetrahedronCells, + #[error("cell key from triangulation has no vertices")] + MissingCellVertices, + #[error("double tetrahedron did not contain the expected shared vertex")] + MissingExpectedSharedVertex, +} #[test] -fn edges_and_incident_edges_on_single_tetrahedron() { +fn edges_and_incident_edges_on_single_tetrahedron() -> Result<(), PublicTopologyApiTestError> { // Single tetrahedron: 4 vertices, 1 cell, 6 unique edges. let vertices = vec![ vertex!([0.0, 0.0, 0.0]), @@ -23,8 +40,7 @@ fn edges_and_incident_edges_on_single_tetrahedron() { DelaunayTriangulation::new_with_topology_guarantee( &vertices, TopologyGuarantee::PLManifold, - ) - .unwrap(); + )?; let tri = dt.as_triangulation(); assert_eq!(dt.number_of_vertices(), 4); @@ -36,12 +52,16 @@ fn edges_and_incident_edges_on_single_tetrahedron() { let edges: std::collections::HashSet<_> = dt.edges().collect(); assert_eq!(edges.len(), 6); - let index = tri.build_adjacency_index().unwrap(); + let index = tri.build_adjacency_index()?; let edges_with_index: std::collections::HashSet<_> = dt.edges_with_index(&index).collect(); assert_eq!(edges_with_index, edges); // Pick an arbitrary vertex; in a tetrahedron its degree is 3. - let v0 = dt.vertices().next().unwrap().0; + let v0 = dt + .vertices() + .next() + .map(|(vertex_key, _)| vertex_key) + .ok_or(PublicTopologyApiTestError::EmptySingleTetrahedronVertices)?; assert_eq!(dt.incident_edges(v0).count(), 3); assert_eq!(dt.incident_edges_with_index(&index, v0).count(), 3); @@ -49,7 +69,11 @@ fn edges_and_incident_edges_on_single_tetrahedron() { assert_eq!(incident.len(), 3); // A single tetrahedron has no cell neighbors. - let cell_key = dt.cells().next().unwrap().0; + let cell_key = dt + .cells() + .next() + .map(|(cell_key, _)| cell_key) + .ok_or(PublicTopologyApiTestError::EmptySingleTetrahedronCells)?; assert_eq!(dt.cell_neighbors(cell_key).count(), 0); assert_eq!(dt.cell_neighbors_with_index(&index, cell_key).count(), 0); @@ -59,11 +83,17 @@ fn edges_and_incident_edges_on_single_tetrahedron() { assert!(dt.vertex_coords(v0).is_some()); assert_eq!(dt.cell_vertices(cell_key), tri.cell_vertices(cell_key)); - assert_eq!(dt.cell_vertices(cell_key).unwrap().len(), 4); + assert_eq!( + dt.cell_vertices(cell_key) + .ok_or(PublicTopologyApiTestError::MissingCellVertices)? + .len(), + 4 + ); + Ok(()) } #[test] -fn adjacency_index_on_double_tetrahedron() { +fn adjacency_index_on_double_tetrahedron() -> Result<(), PublicTopologyApiTestError> { // Two tetrahedra sharing a triangular facet. let vertices: Vec<_> = vec![ // Shared triangle @@ -79,8 +109,7 @@ fn adjacency_index_on_double_tetrahedron() { DelaunayTriangulation::new_with_topology_guarantee( &vertices, TopologyGuarantee::PLManifold, - ) - .unwrap(); + )?; let tri = dt.as_triangulation(); assert_eq!(tri.number_of_vertices(), 5); @@ -93,7 +122,7 @@ fn adjacency_index_on_double_tetrahedron() { let coords = dt.vertex_coords(vk)?; (coords == [0.0, 0.0, 0.0]).then_some(vk) }) - .unwrap(); + .ok_or(PublicTopologyApiTestError::MissingExpectedSharedVertex)?; // The shared vertex should be incident to both cells. assert_eq!(tri.adjacent_cells(shared_vertex_key).count(), 2); @@ -110,7 +139,7 @@ fn adjacency_index_on_double_tetrahedron() { } // Build opt-in adjacency index and validate key properties. - let index = tri.build_adjacency_index().unwrap(); + let index = tri.build_adjacency_index()?; // Triangulation-level with_index helpers should match the index and the baseline APIs. assert_eq!( @@ -177,4 +206,5 @@ fn adjacency_index_on_double_tetrahedron() { .count(), 0 ); + Ok(()) } diff --git a/tests/semgrep/src/project_rules/rust_style.rs b/tests/semgrep/src/project_rules/rust_style.rs index f87b90ab..a645a11a 100644 --- a/tests/semgrep/src/project_rules/rust_style.rs +++ b/tests/semgrep/src/project_rules/rust_style.rs @@ -35,17 +35,23 @@ pub fn safe_conversion_fallback(value: u64) -> f64 { } pub fn public_unwrap_bypass(value: Option) -> u8 { - // ruleid: delaunay.rust.no-production-unwrap-panic + // ruleid: delaunay.rust.no-production-unwrap-panic, delaunay.rust.no-public-surface-unwrap-panic value.unwrap() } pub fn public_expect_bypass(value: Option) -> u8 { - // ruleid: delaunay.rust.no-production-unwrap-panic + // ruleid: delaunay.rust.no-production-unwrap-panic, delaunay.rust.no-public-surface-unwrap-panic value.expect("public APIs should return typed errors instead") } +pub fn public_panic_bypass() { + // ruleid: delaunay.rust.no-production-unwrap-panic, delaunay.rust.no-public-surface-unwrap-panic + panic!("public APIs should return typed errors instead"); +} + fn private_documented_invariant(value: Option) -> u8 { // ok: delaunay.rust.no-production-unwrap-panic + // ruleid: delaunay.rust.no-public-surface-unwrap-panic value.expect("private helper documents an internal invariant") }