Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

149 changes: 93 additions & 56 deletions benches/ci_performance_suite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,18 @@ fn operation_benchmark_ids(group: &str, prefix: &str) -> String {

fn validation_benchmark_ids() -> String {
[
format!("validation/{{validate_3d,validate_3d_adversarial}}/{CANARY_COUNT_3D}"),
format!("validation/{{validate_4d,validate_4d_adversarial}}/{CANARY_COUNT_4D}"),
format!("validation/{{validate_5d,validate_5d_adversarial}}/{CANARY_COUNT_5D}"),
format!(
"validation/{{validate_2d,validate_2d_adversarial,is_valid_delaunay_2d,is_valid_delaunay_2d_adversarial,delaunay_report_2d,delaunay_report_2d_adversarial}}/{CANARY_COUNT_2D}"
),
format!(
"validation/{{validate_3d,validate_3d_adversarial,is_valid_delaunay_3d,is_valid_delaunay_3d_adversarial,delaunay_report_3d,delaunay_report_3d_adversarial}}/{CANARY_COUNT_3D}"
),
format!(
"validation/{{validate_4d,validate_4d_adversarial,is_valid_delaunay_4d,is_valid_delaunay_4d_adversarial,delaunay_report_4d,delaunay_report_4d_adversarial}}/{CANARY_COUNT_4D}"
),
format!(
"validation/{{validate_5d,validate_5d_adversarial,is_valid_delaunay_5d,is_valid_delaunay_5d_adversarial,delaunay_report_5d,delaunay_report_5d_adversarial}}/{CANARY_COUNT_5D}"
),
]
.join(";")
}
Expand Down Expand Up @@ -238,10 +247,10 @@ fn api_benchmark_entries() -> Vec<ApiBenchmarkEntry> {
},
ApiBenchmarkEntry {
group: "validation",
public_api: "DelaunayTriangulation::validate",
dimensions: "3,4,5",
public_api: "DelaunayTriangulation::{validate,is_valid_delaunay,delaunay_report}",
dimensions: "2,3,4,5",
benchmark_ids: validation_benchmark_ids(),
note: "cumulative_levels_1_through_5_on_well_conditioned_and_adversarial_inputs",
note: "cumulative_and_level_5_report_validation_on_well_conditioned_and_adversarial_inputs",
},
ApiBenchmarkEntry {
group: "incremental_insert",
Expand Down Expand Up @@ -1306,6 +1315,48 @@ fn bench_validate_case<const D: usize>(
);
}

fn bench_delaunay_validation_case<const D: usize>(
group: &mut BenchmarkGroup<'_, WallTime>,
dimension: usize,
dataset: Dataset,
count: usize,
dt: &BenchTriangulation<D>,
) {
group.throughput(Throughput::Elements(count as u64));
group.bench_function(
BenchmarkId::new(
format!("is_valid_delaunay_{dimension}d{}", dataset.suffix()),
count,
),
|b| {
b.iter(|| match black_box(dt.is_valid_delaunay()) {
Ok(()) => {}
Err(error) => {
abort_benchmark(format_args!(
"{dimension}D benchmark triangulation should be Delaunay: {error}"
));
}
});
},
);
group.bench_function(
BenchmarkId::new(
format!("delaunay_report_{dimension}d{}", dataset.suffix()),
count,
),
|b| {
b.iter(|| match black_box(dt.delaunay_report()) {
Ok(()) => {}
Err(error) => {
abort_benchmark(format_args!(
"{dimension}D benchmark triangulation should have a valid Delaunay report: {error:?}"
));
}
});
},
);
}

fn bench_insert_case<const D: usize>(
group: &mut BenchmarkGroup<'_, WallTime>,
dimension: usize,
Expand Down Expand Up @@ -1557,6 +1608,38 @@ fn benchmark_convex_hull_queries(c: &mut Criterion) {
group.finish();
}

fn bench_validation_dimension<const D: usize>(
group: &mut BenchmarkGroup<'_, WallTime>,
dimension: usize,
seed: u64,
count: usize,
include_cumulative_validation: bool,
) {
let triangulation = prepare_dt::<D>(seed, count);
if include_cumulative_validation {
bench_validate_case(
group,
dimension,
Dataset::WellConditioned,
count,
&triangulation,
);
}
bench_delaunay_validation_case(
group,
dimension,
Dataset::WellConditioned,
count,
&triangulation,
);

let adversarial = prepare_adv_dt::<D>(seed, count);
if include_cumulative_validation {
bench_validate_case(group, dimension, Dataset::Adversarial, count, &adversarial);
}
bench_delaunay_validation_case(group, dimension, Dataset::Adversarial, count, &adversarial);
}

fn benchmark_validation(c: &mut Criterion) {
print_manifest_once();
if discover_seeds_enabled() {
Expand All @@ -1565,56 +1648,10 @@ fn benchmark_validation(c: &mut Criterion) {
let mut group = c.benchmark_group("validation");
group.sample_size(15);

let dt_3d = prepare_dt::<3>(123, CANARY_COUNT_3D);
bench_validate_case(
&mut group,
3,
Dataset::WellConditioned,
CANARY_COUNT_3D,
&dt_3d,
);
let dt_3d_adversarial = prepare_adv_dt::<3>(123, CANARY_COUNT_3D);
bench_validate_case(
&mut group,
3,
Dataset::Adversarial,
CANARY_COUNT_3D,
&dt_3d_adversarial,
);

let dt_4d = prepare_dt::<4>(456, CANARY_COUNT_4D);
bench_validate_case(
&mut group,
4,
Dataset::WellConditioned,
CANARY_COUNT_4D,
&dt_4d,
);
let dt_4d_adversarial = prepare_adv_dt::<4>(456, CANARY_COUNT_4D);
bench_validate_case(
&mut group,
4,
Dataset::Adversarial,
CANARY_COUNT_4D,
&dt_4d_adversarial,
);

let dt_5d = prepare_dt::<5>(789, CANARY_COUNT_5D);
bench_validate_case(
&mut group,
5,
Dataset::WellConditioned,
CANARY_COUNT_5D,
&dt_5d,
);
let dt_5d_adversarial = prepare_adv_dt::<5>(789, CANARY_COUNT_5D);
bench_validate_case(
&mut group,
5,
Dataset::Adversarial,
CANARY_COUNT_5D,
&dt_5d_adversarial,
);
bench_validation_dimension::<2>(&mut group, 2, 42, CANARY_COUNT_2D, true);
bench_validation_dimension::<3>(&mut group, 3, 123, CANARY_COUNT_3D, true);
bench_validation_dimension::<4>(&mut group, 4, 456, CANARY_COUNT_4D, true);
bench_validation_dimension::<5>(&mut group, 5, 789, CANARY_COUNT_5D, true);

group.finish();
}
Expand Down
19 changes: 14 additions & 5 deletions docs/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -767,15 +767,22 @@ enough for future regular, weighted, Gabriel, alpha, constrained, or related pre
- **Time**:
- `DelaunayTriangulation::is_valid_delaunay()` (Level 5 only): O(simplices) local flip-predicate verification.
- `DelaunayTriangulation::validate()` (Levels 1–5): Levels 1-4 plus O(simplices) local flip-predicate verification.
- `DelaunayTriangulation::validation_report()` (Levels 1–5): Levels 1-4 plus O(simplices) local flip-predicate verification.
- **Space**: O(1) additional space (aside from temporary working sets)
- `DelaunayTriangulation::delaunay_report()` (Level 5 only): O(simplices) when complete Euclidean
point-set provenance and robust local predicates certify a valid report; O(simplices × vertices)
for the exact fallback when Euclidean connectivity is unproven or the local certificate fails or
is inconclusive.
- `DelaunayTriangulation::validation_report()` (Levels 1–5): Levels 1-4 plus the certified
O(simplices) Level 5 path, or Levels 1-4 plus the O(simplices × vertices) Level 5 fallback.
- **Space**: O(1) validation workspace for the local verifier; report output and fallback working sets
scale with the number of reported violations.

### When to Use

- **Critical Applications**: When Delaunay guarantees are essential (interpolation, mesh quality)
- **Tests**: After construction to verify correctness
- **Debug**: Investigating geometric issues or suspected violations
- **Avoid**: Hot loops (still O(simplices); use for spot checks / tests)
- **Avoid**: Report generation in hot loops, especially when connectivity is unproven and may require the
O(simplices × vertices) fallback. The boolean `is_valid_delaunay()` check remains O(simplices).

### Example

Expand Down Expand Up @@ -841,7 +848,9 @@ Start: Do you need to validate?
intersections, using bounding boxes before exact rational witness construction.
- Level 5 `DelaunayTriangulation::is_valid_delaunay()` verifies the implemented Delaunay predicate
family via local flip predicates after Level 4 realization validation.
- A brute-force empty-circumsphere check would be O(simplices × vertices) and is not used by `is_valid_delaunay()`.
- The O(simplices × vertices) brute-force empty-circumsphere check is not used by
`is_valid_delaunay()`, but report APIs use it when connectivity lacks complete-point-set provenance or
the local certificate fails or is inconclusive.

In practice, `DelaunayTriangulation::validate()` is usually dominated by Level 3 Intrinsic PL Topology work or
Level 4 pairwise realization checks, depending on mesh size and overlap candidates.
Expand Down Expand Up @@ -1014,7 +1023,7 @@ converge, consider the opt-in heuristic rebuild fallback via
| 4 | `Triangulation::validate_realization()` | `triangulation` | O(simplices × D²) + O(simplices² × f(D)) |
| 5 | `DelaunayTriangulation::is_valid_delaunay()` | `delaunay` | O(simplices) |
| 5 | `DelaunayTriangulation::validate()` | `delaunay` | Levels 1-4 + O(simplices) |
| — | `DelaunayTriangulation::validation_report()` | `delaunay` | Levels 1-4 + O(simplices) |
| — | `DelaunayTriangulation::validation_report()` | `delaunay` | Levels 1-4 + O(simplices) certified fast path; O(simplices × vertices) fallback |

---

Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ taplo_version := "0.10.0"
tectonic_version := "0.17.0"
tex_fmt_version := "0.5.7"
typos_version := "1.49.0"
uv_version := "0.12.3"
uv_version := "0.12.4"
zizmor_version := "1.29.0"

# Common cargo-llvm-cov arguments for all coverage runs.
Expand Down
25 changes: 24 additions & 1 deletion src/core/algorithms/flips.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ use crate::core::triangulation::Triangulation;
use crate::core::util::stable_hash_u64_slice;
use crate::core::validation::{TopologyGuarantee, TriangulationValidationError};
use crate::core::vertex::Vertex;
use crate::geometry::kernel::Kernel;
use crate::geometry::kernel::{Kernel, RobustKernel};
use crate::geometry::point::Point;
use crate::geometry::predicates::{Orientation, simplex_orientation_fast_filter_sign};
use crate::geometry::robust_predicates::robust_orientation;
Expand Down Expand Up @@ -7423,6 +7423,29 @@ where
)
}

/// Verifies a complete Euclidean point-set triangulation with robust local predicates.
///
/// This is the certificate predicate used by the structured Level 5 report.
/// It deliberately ignores the owner's generic kernel because the report's
/// fallback oracle is defined by the unperturbed robust empty-sphere predicate.
/// Callers must separately prove that the complex triangulates the complete
/// Euclidean point set; local predicates are not a global certificate for
/// arbitrary explicit or constrained connectivity.
///
/// # Errors
///
/// Returns any [`DelaunayRepairError`] surfaced by the topology-aware local
/// verifier, including predicate, connectivity, and postcondition failures.
pub(crate) fn verify_complete_euclidean_tds_via_robust_flip_predicates<U, V, const D: usize>(
tds: &Tds<U, V, D>,
) -> Result<(), DelaunayRepairError>
where
U: DataType,
V: DataType,
{
verify_delaunay_with_topology(tds, &RobustKernel::new(), GlobalTopology::Euclidean)
}

/// Verify the Delaunay property via local flip predicates under a global topology model.
///
/// For periodic topologies this evaluates predicates in lifted coordinates using the
Expand Down
16 changes: 13 additions & 3 deletions src/delaunay/construction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ use crate::topology::traits::{
ToroidalDomainError,
};
use crate::triangulation::DelaunayTriangulation;
use crate::triangulation::EuclideanDelaunayReportDomain;
use crate::validation::{DelaunayTriangulationValidationError, DelaunayVerificationError};
use core::{cmp::Ordering, fmt};
use num_traits::ToPrimitive;
Expand Down Expand Up @@ -4031,6 +4032,7 @@ where
},
insertion_state: DelaunayInsertionState::new(),
spatial_index: None,
euclidean_report_domain: EuclideanDelaunayReportDomain::CompletePointSet,
};

// During batch construction, use suspicion-driven validation instead of
Expand Down Expand Up @@ -4163,6 +4165,7 @@ where
},
insertion_state: DelaunayInsertionState::new(),
spatial_index: None,
euclidean_report_domain: EuclideanDelaunayReportDomain::CompletePointSet,
};

// During batch construction, use suspicion-driven validation instead of
Expand Down Expand Up @@ -5068,9 +5071,11 @@ where

/// Creates an empty Delaunay wrapper with explicit validation and topology context.
///
/// Repair and builder paths use this before inserting vertices so subsequent
/// topology validation observes the same global topology as the source
/// triangulation or construction mode.
/// Repair and builder paths use this before incrementally inserting every
/// vertex, so Euclidean callers establish a complete point-set domain while
/// subsequent topology validation observes the same global topology as the
/// source triangulation or construction mode. Callers that populate the TDS
/// through another assembly path must reset the report domain to `Unproven`.
pub(crate) fn with_empty_kernel_and_topology_context(
kernel: K,
topology_guarantee: TopologyGuarantee,
Expand All @@ -5086,6 +5091,11 @@ where
),
insertion_state: DelaunayInsertionState::new(),
spatial_index: HashGridIndex::try_new(duplicate_tolerance).ok(),
euclidean_report_domain: if global_topology.is_euclidean() {
EuclideanDelaunayReportDomain::CompletePointSet
} else {
EuclideanDelaunayReportDomain::Unproven
},
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/delaunay/deletion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ where
if let Some(index) = delaunay.spatial_index.as_mut() {
index.remove_vertex(&vertex_key, &removed_vertex_coords);
}
delaunay.invalidate_euclidean_report_domain();
transaction.commit();
Ok(simplices_removed)
}
Expand Down Expand Up @@ -646,6 +647,10 @@ mod tests {
dt.spatial_index = Some(spatial_index);
assert!(dt.insertion_state.last_inserted_simplex.is_some());
assert!(dt.spatial_index.is_some());
assert!(
dt.euclidean_report_domain.supports_local_certificate(),
"incremental construction should establish the report domain"
);

dt.set_delaunay_repair_policy(DelaunayRepairPolicy::Never);
let removed_simplices = dt.delete_vertex(vertex_key).unwrap();
Expand All @@ -665,6 +670,10 @@ mod tests {
);
assert!(!found_removed_key);
assert!(dt.as_triangulation().validate().is_ok());
assert!(
!dt.euclidean_report_domain.supports_local_certificate(),
"vertex deletion must revoke the complete-point-set proof"
);
}

#[test]
Expand Down
Loading
Loading