#![forbid(unsafe_code)]
use crate::core::algorithms::flips::{
DelaunayRepairError, verify_complete_euclidean_tds_via_robust_flip_predicates,
verify_tds_via_flip_predicates_assuming_connected,
};
use crate::core::collections::ViolationBuffer;
use crate::core::tds::{SimplexKey, Tds, TdsError, TopologyOwnerId};
use crate::delaunay_model::DelaunayTriangulation;
#[cfg(feature = "diagnostics")]
use crate::delaunay_property_validation::debug_print_first_delaunay_violation as debug_print_first_tds_delaunay_violation;
use crate::delaunay_property_validation::{
DelaunayValidationError, DelaunayViolationReport,
delaunay_violation_report as tds_delaunay_violation_report, is_delaunay_property_only,
};
use crate::draft::DelaunayTriangulationDraft;
use crate::geometry::kernel::Kernel;
use crate::refinement::RefinementError;
use crate::repair::DelaunayRepairOperation;
use crate::topology::traits::topological_space::GlobalTopology;
use crate::triangulation::Triangulation;
use crate::triangulation::builder::{
TriangulationBuildFailure, TriangulationBuilder, TriangulationBuilderError,
};
use crate::triangulation::realization::TriangulationRealizationValidationError;
use crate::triangulation::validation::{
TopologyCertificationEvidence, TopologyConstructionProvenance, TopologyGuarantee,
TriangulationValidationError,
};
pub use crate::validation_report::{
InvariantError, InvariantKind, InvariantViolation, TriangulationValidationReport,
};
use std::num::NonZeroUsize;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DelaunayValidationErrorKind {
Tds,
Triangulation,
Realization,
VerificationFailed,
RepairOperationFailed,
}
impl From<&DelaunayTriangulationValidationError> for DelaunayValidationErrorKind {
fn from(source: &DelaunayTriangulationValidationError) -> Self {
match source {
DelaunayTriangulationValidationError::Tds { source: _ } => Self::Tds,
DelaunayTriangulationValidationError::Triangulation { source: _ } => {
Self::Triangulation
}
DelaunayTriangulationValidationError::Realization { source: _ } => Self::Realization,
DelaunayTriangulationValidationError::VerificationFailed { .. } => {
Self::VerificationFailed
}
DelaunayTriangulationValidationError::RepairOperationFailed { .. } => {
Self::RepairOperationFailed
}
}
}
}
impl<K, U, V, const D: usize> Triangulation<K, U, V, D> {
pub fn delaunay_violation_report(
&self,
simplices_to_check: Option<&[SimplexKey]>,
) -> Result<DelaunayViolationReport, DelaunayValidationError> {
tds_delaunay_violation_report(self.tds(), simplices_to_check)
}
#[cfg(feature = "diagnostics")]
#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
pub fn debug_print_first_delaunay_violation(&self, simplices_subset: Option<&[SimplexKey]>) {
debug_print_first_tds_delaunay_violation(self.tds(), simplices_subset);
}
}
#[derive(Clone, Debug)]
pub(crate) struct DelaunayLevelFiveCertificate<const D: usize> {
owner_id: TopologyOwnerId,
generation: u64,
global_topology: GlobalTopology<D>,
evidence: DelaunayLevelFiveEvidence,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DelaunayLevelFiveEvidence {
EuclideanEmptyCircumsphere,
FlipPredicates,
}
impl<const D: usize> DelaunayLevelFiveCertificate<D> {
fn for_triangulation<K, U, V>(
triangulation: &Triangulation<K, U, V, D>,
evidence: DelaunayLevelFiveEvidence,
) -> Self {
Self {
owner_id: triangulation.tds.topology_owner_id(),
generation: triangulation.tds.generation(),
global_topology: triangulation.global_topology,
evidence,
}
}
pub(crate) fn applies_to<K, U, V>(&self, triangulation: &Triangulation<K, U, V, D>) -> bool {
self.owner_id == triangulation.tds.topology_owner_id()
&& self.generation == triangulation.tds.generation()
&& self.global_topology == triangulation.global_topology
&& match self.evidence {
DelaunayLevelFiveEvidence::EuclideanEmptyCircumsphere => {
triangulation.global_topology.is_euclidean()
}
DelaunayLevelFiveEvidence::FlipPredicates => {
!triangulation.global_topology.is_euclidean()
}
}
}
}
pub(crate) fn certify_level_five_for_refinement<K, U, V, const D: usize>(
triangulation: &Triangulation<K, U, V, D>,
) -> Result<DelaunayLevelFiveCertificate<D>, DelaunayTriangulationValidationError>
where
K: Kernel<D, Scalar = f64>,
{
if triangulation.global_topology().is_euclidean() {
is_delaunay_property_only(&triangulation.tds).map_err(|source| {
DelaunayTriangulationValidationError::VerificationFailed {
source: Box::new(DelaunayVerificationError::from(source)),
}
})?;
} else {
verify_tds_via_flip_predicates_assuming_connected(
&triangulation.tds,
&triangulation.kernel,
triangulation.global_topology,
)
.map_err(
|source| DelaunayTriangulationValidationError::VerificationFailed {
source: Box::new(DelaunayVerificationError::from(source)),
},
)?;
}
let evidence = if triangulation.global_topology().is_euclidean() {
DelaunayLevelFiveEvidence::EuclideanEmptyCircumsphere
} else {
DelaunayLevelFiveEvidence::FlipPredicates
};
Ok(DelaunayLevelFiveCertificate::for_triangulation(
triangulation,
evidence,
))
}
#[derive(Clone, Debug, Error, PartialEq)]
#[non_exhaustive]
pub enum DelaunayVerificationError {
#[error("flip-predicate verification failed: {source}")]
FlipPredicates {
#[source]
source: Box<DelaunayRepairError>,
},
#[error("empty-circumsphere validation failed: {source}")]
EmptyCircumsphere {
#[source]
source: Box<DelaunayValidationError>,
},
}
impl From<DelaunayRepairError> for DelaunayVerificationError {
fn from(source: DelaunayRepairError) -> Self {
Self::FlipPredicates {
source: Box::new(source),
}
}
}
impl From<DelaunayValidationError> for DelaunayVerificationError {
fn from(source: DelaunayValidationError) -> Self {
Self::EmptyCircumsphere {
source: Box::new(source),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DelaunayVerificationErrorKind {
FlipPredicates,
EmptyCircumsphere,
}
impl From<&DelaunayVerificationError> for DelaunayVerificationErrorKind {
fn from(source: &DelaunayVerificationError) -> Self {
match source {
DelaunayVerificationError::FlipPredicates { .. } => Self::FlipPredicates,
DelaunayVerificationError::EmptyCircumsphere { .. } => Self::EmptyCircumsphere,
}
}
}
#[derive(Clone, Debug, Error, PartialEq)]
#[non_exhaustive]
pub enum DelaunayTriangulationValidationError {
#[error(transparent)]
Tds {
source: Box<TdsError>,
},
#[error(transparent)]
Triangulation {
source: Box<TriangulationValidationError>,
},
#[error(transparent)]
Realization {
source: Box<TriangulationRealizationValidationError>,
},
#[error("Delaunay verification failed: {source}")]
VerificationFailed {
#[source]
source: Box<DelaunayVerificationError>,
},
#[error("Delaunay repair failed during {operation}: {source}")]
RepairOperationFailed {
operation: DelaunayRepairOperation,
#[source]
source: Box<DelaunayRepairError>,
},
}
impl From<TdsError> for DelaunayTriangulationValidationError {
fn from(source: TdsError) -> Self {
Self::Tds {
source: Box::new(source),
}
}
}
impl From<TriangulationValidationError> for DelaunayTriangulationValidationError {
fn from(source: TriangulationValidationError) -> Self {
Self::Triangulation {
source: Box::new(source),
}
}
}
impl From<TriangulationRealizationValidationError> for DelaunayTriangulationValidationError {
fn from(source: TriangulationRealizationValidationError) -> Self {
match source {
TriangulationRealizationValidationError::Tds { source } => Self::Tds { source },
TriangulationRealizationValidationError::Triangulation { source } => {
Self::Triangulation { source }
}
source => Self::Realization {
source: Box::new(source),
},
}
}
}
pub type DelaunayTriangulationRefinementError<K, U, V, const D: usize> =
RefinementError<Triangulation<K, U, V, D>, DelaunayTriangulationValidationError>;
#[derive(Clone, Debug, Error)]
#[non_exhaustive]
pub(crate) enum DelaunayTdsRestorationError<K, U, V, const D: usize> {
#[error("TDS-to-triangulation refinement failed: {failure}")]
Triangulation {
#[source]
failure: TriangulationBuildFailure<U, V, D>,
},
#[error("triangulation-to-Delaunay refinement failed: {failure}")]
Delaunay {
#[source]
failure: DelaunayTriangulationRefinementError<K, U, V, D>,
},
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum DelaunayTdsRestorationReason {
Triangulation {
source: TriangulationBuilderError,
},
Delaunay {
source: DelaunayTriangulationValidationError,
},
}
impl<K, U, V, const D: usize> DelaunayTdsRestorationError<K, U, V, D> {
pub(crate) fn into_reason(self) -> DelaunayTdsRestorationReason {
match self {
Self::Triangulation { failure } => DelaunayTdsRestorationReason::Triangulation {
source: failure.into_reason(),
},
Self::Delaunay { failure } => DelaunayTdsRestorationReason::Delaunay {
source: failure.into_reason(),
},
}
}
}
#[must_use = "validation cadence values only affect diagnostics when they are used"]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidationCadence {
Never,
EveryN(NonZeroUsize),
}
impl ValidationCadence {
pub const fn from_optional_every(validate_every: Option<usize>) -> Self {
match validate_every {
None | Some(0) => Self::Never,
Some(every) => {
if let Some(every) = NonZeroUsize::new(every) {
Self::EveryN(every)
} else {
Self::Never
}
}
}
}
#[must_use]
pub const fn should_validate(self, insertion_count: usize) -> bool {
match self {
Self::Never => false,
Self::EveryN(every) => {
insertion_count != 0 && insertion_count.is_multiple_of(every.get())
}
}
}
}
impl<K, U, V, const D: usize> DelaunayTriangulation<K, U, V, D>
where
K: Kernel<D, Scalar = f64>,
{
pub fn is_valid_delaunay(&self) -> Result<(), DelaunayTriangulationValidationError> {
if self.global_topology().is_euclidean() {
if self.euclidean_report_domain.supports_local_certificate()
&& verify_complete_euclidean_tds_via_robust_flip_predicates(&self.tri.tds).is_ok()
{
return Ok(());
}
return is_delaunay_property_only(&self.tri.tds).map_err(|source| {
DelaunayTriangulationValidationError::VerificationFailed {
source: Box::new(DelaunayVerificationError::from(source)),
}
});
}
self.verify_via_flip_predicates().map_err(|source| {
DelaunayTriangulationValidationError::VerificationFailed {
source: Box::new(DelaunayVerificationError::from(source)),
}
})
}
#[must_use]
pub fn delaunay_diagnostic(&self) -> Option<InvariantViolation> {
self.delaunay_report()
.err()
.and_then(|report| report.violations.into_iter().next())
}
pub fn delaunay_report(&self) -> Result<(), TriangulationValidationReport> {
if self.global_topology().is_euclidean() {
return match self.delaunay_violation_report(None) {
Ok(report) if report.is_valid() => Ok(()),
Ok(report) => Err(TriangulationValidationReport {
violations: report
.violation_details
.into_iter()
.map(|detail| InvariantViolation {
kind: InvariantKind::DelaunayProperty,
error: InvariantError::Delaunay {
source: DelaunayTriangulationValidationError::VerificationFailed {
source: Box::new(DelaunayVerificationError::from(
DelaunayValidationError::from(detail),
)),
},
},
})
.collect(),
}),
Err(source) => Err(TriangulationValidationReport {
violations: vec![InvariantViolation {
kind: InvariantKind::DelaunayProperty,
error: InvariantError::Delaunay {
source: DelaunayTriangulationValidationError::VerificationFailed {
source: Box::new(DelaunayVerificationError::from(source)),
},
},
}],
}),
};
}
self.is_valid_delaunay()
.map_err(|error| TriangulationValidationReport {
violations: vec![InvariantViolation {
kind: InvariantKind::DelaunayProperty,
error: InvariantError::Delaunay { source: error },
}],
})
}
pub fn delaunay_violation_report(
&self,
simplices_to_check: Option<&[SimplexKey]>,
) -> Result<DelaunayViolationReport, DelaunayValidationError> {
if simplices_to_check.is_none()
&& self.global_topology().is_euclidean()
&& self.euclidean_report_domain.supports_local_certificate()
&& verify_complete_euclidean_tds_via_robust_flip_predicates(&self.tri.tds).is_ok()
{
return Ok(DelaunayViolationReport {
number_of_vertices: self.number_of_vertices(),
number_of_simplices: self.number_of_simplices(),
checked_simplices: self.number_of_simplices(),
violating_simplices: ViolationBuffer::new(),
violation_details: Vec::new(),
});
}
tds_delaunay_violation_report(&self.tri.tds, simplices_to_check)
}
#[cfg(feature = "diagnostics")]
#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
pub fn debug_print_first_delaunay_violation(&self, simplices_subset: Option<&[SimplexKey]>) {
debug_print_first_tds_delaunay_violation(&self.tri.tds, simplices_subset);
}
pub fn verify_via_flip_predicates(&self) -> Result<(), DelaunayRepairError> {
verify_tds_via_flip_predicates_assuming_connected(
&self.tri.tds,
&self.tri.kernel,
self.tri.global_topology,
)
}
pub fn validate(&self) -> Result<(), DelaunayTriangulationValidationError> {
self.tri.validate_realization()?;
self.is_valid_delaunay()
}
pub fn validation_report(&self) -> Result<(), TriangulationValidationReport> {
match self.tri.validation_report() {
Ok(()) => {
let realization_report = self.tri.realization_report().map_err(|error| {
TriangulationValidationReport {
violations: vec![InvariantViolation {
kind: InvariantKind::Realization,
error: error.into(),
}],
}
})?;
let mut violations = Vec::new();
if !realization_report.is_valid() {
violations.extend(realization_report.violations.into_iter().map(|error| {
InvariantViolation {
kind: InvariantKind::Realization,
error: error.into(),
}
}));
}
if let Err(delaunay_report) = self.delaunay_report() {
violations.extend(delaunay_report.violations);
}
if violations.is_empty() {
Ok(())
} else {
Err(TriangulationValidationReport { violations })
}
}
Err(mut report) => {
if report.violations.iter().any(|v| {
matches!(
v.kind,
InvariantKind::VertexMappings | InvariantKind::SimplexMappings
)
}) {
return Err(report);
}
match self.tri.realization_report() {
Ok(realization_report) => {
report
.violations
.extend(realization_report.violations.into_iter().map(|error| {
InvariantViolation {
kind: InvariantKind::Realization,
error: InvariantError::Realization { source: error },
}
}));
}
Err(source) => {
report.violations.push(InvariantViolation {
kind: InvariantKind::Realization,
error: InvariantError::Realization { source },
});
}
}
if let Err(delaunay_report) = self.delaunay_report() {
report.violations.extend(delaunay_report.violations);
}
if report.violations.is_empty() {
Ok(())
} else {
Err(report)
}
}
}
}
pub(crate) fn try_restore_with_provenance(
tds: Tds<U, V, D>,
kernel: K,
topology_guarantee: TopologyGuarantee,
global_topology: GlobalTopology<D>,
construction_provenance: TopologyConstructionProvenance,
) -> Result<(Self, TopologyCertificationEvidence), DelaunayTdsRestorationError<K, U, V, D>>
where
U: Clone,
V: Clone,
{
let (triangulation, topology_evidence) = TriangulationBuilder::new(tds, kernel)
.topology_guarantee(topology_guarantee)
.global_topology(global_topology)
.construction_provenance(construction_provenance)
.build_with_topology_evidence()
.map_err(|failure| DelaunayTdsRestorationError::Triangulation { failure })?;
let triangulation = DelaunayTriangulationDraft::from_triangulation(triangulation)
.try_into_delaunay()
.map_err(|failure| DelaunayTdsRestorationError::Delaunay { failure })?;
Ok((triangulation, topology_evidence))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::DelaunayTriangulationBuilder;
use crate::core::algorithms::flips::{
DelaunayRepairDiagnostics, DelaunayRepairPostconditionFailure, RepairQueueOrder,
};
use crate::core::tds::{TdsBuilder, VertexKey};
use crate::core::traits::data_type::DataType;
use crate::core::vertex::Vertex;
use crate::delaunay_model::EuclideanDelaunayReportDomain;
use crate::geometry::coordinate_range::CoordinateRange;
use crate::geometry::kernel::{AdaptiveKernel, ExactPredicates};
use crate::geometry::point::Point;
use crate::geometry::traits::coordinate::CoordinateConversionError;
use crate::geometry::util::generate_random_points_in_range_seeded;
use crate::vertex;
use slotmap::KeyData;
use std::{assert_matches, error::Error, sync::Once};
use uuid::Uuid;
impl<K, U, V, const D: usize> DelaunayTriangulation<K, U, V, D>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
{
pub(crate) fn try_restore_from_tds_with_topology_context(
tds: Tds<U, V, D>,
kernel: K,
topology_guarantee: TopologyGuarantee,
global_topology: GlobalTopology<D>,
) -> Result<Self, DelaunayTdsRestorationError<K, U, V, D>> {
Self::try_restore_with_provenance(
tds,
kernel,
topology_guarantee,
global_topology,
TopologyConstructionProvenance::Unproven,
)
.map(|(triangulation, _evidence)| triangulation)
}
}
#[derive(Clone, Debug)]
struct PanickingKernel;
impl<const D: usize> Kernel<D> for PanickingKernel {
type Scalar = f64;
fn orientation(&self, _points: &[Point<D>]) -> Result<i32, CoordinateConversionError> {
panic!("the structured report must not call its owner's kernel")
}
fn in_sphere(
&self,
_simplex_points: &[Point<D>],
_test_point: &Point<D>,
) -> Result<i32, CoordinateConversionError> {
panic!("the structured report must not call its owner's kernel")
}
}
fn test_vertex<const D: usize>(coords: [f64; D]) -> Vertex<(), D> {
vertex!(coords).unwrap()
}
fn init_tracing() {
static INIT: Once = Once::new();
INIT.call_once(|| {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn"));
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_test_writer()
.try_init();
});
}
fn non_delaunay_quad_tds() -> Tds<(), (), 2> {
let vertices = [
test_vertex([0.0, 0.0]),
test_vertex([4.0, 0.0]),
test_vertex([4.0, 2.0]),
test_vertex([1.0, 2.0]),
];
let simplices = [vec![0, 1, 2], vec![0, 2, 3]];
TdsBuilder::new(&vertices, &simplices).build().unwrap()
}
fn isolated_vertex_tds_3d() -> Tds<(), (), 3> {
let vertices = [
test_vertex([0.0, 0.0, 0.0]),
test_vertex([1.0, 0.0, 0.0]),
test_vertex([0.0, 1.0, 0.0]),
test_vertex([0.0, 0.0, 1.0]),
];
let dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let mut tds = dt.into_triangulation().into_tds();
tds.insert_vertex_with_mapping(test_vertex([0.5, 0.5, 0.5]))
.unwrap();
tds
}
fn tds_from_2d_vertices_and_simplices(
coords: &[[f64; 2]],
simplices: &[Vec<usize>],
) -> Tds<(), (), 2> {
let vertices: Vec<_> = coords.iter().copied().map(test_vertex).collect();
TdsBuilder::new(&vertices, simplices).build().unwrap()
}
fn unchecked_test_delaunay_from_tds<const D: usize>(
tds: Tds<(), (), D>,
) -> DelaunayTriangulation<AdaptiveKernel<f64>, (), (), D> {
DelaunayTriangulationDraft::assemble_unchecked_for_test(
tds,
AdaptiveKernel::new(),
TopologyGuarantee::Pseudomanifold,
GlobalTopology::Euclidean,
)
.into_unproven_delaunay_for_test()
}
fn synthetic_flip_verification_source(message: &str) -> DelaunayVerificationError {
let _ = message;
DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed {
reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected { simplex_count: 1 }),
})
}
fn randomized_delaunay<const D: usize>(
base_seed: u64,
) -> DelaunayTriangulation<AdaptiveKernel<f64>, (), (), D>
where
AdaptiveKernel<f64>: ExactPredicates<D>,
{
let bounds = CoordinateRange::try_new(-10.0, 10.0).unwrap();
for offset in 0..32 {
let points = generate_random_points_in_range_seeded::<D>(
D + 8,
bounds,
base_seed.wrapping_add(offset),
)
.unwrap();
let vertices: Vec<_> = points
.iter()
.map(|point| test_vertex(*point.coords()))
.collect();
if let Ok(triangulation) = DelaunayTriangulationBuilder::new(&vertices).build() {
return triangulation;
}
}
panic!("failed to build seeded randomized {D}D Delaunay test fixture");
}
fn shared_facet_flip_adversary<const D: usize>() -> Triangulation<AdaptiveKernel<f64>, (), (), D>
where
AdaptiveKernel<f64>: ExactPredicates<D>,
{
let dim = u32::try_from(D).unwrap();
let high_apex_coordinate = 1.1 / f64::from(dim);
let mut vertices = Vec::with_capacity(D + 2);
vertices.push(test_vertex([0.0; D]));
for axis in 0..D {
let mut coordinates = [0.0; D];
coordinates[axis] = 1.0;
vertices.push(test_vertex(coordinates));
}
vertices.push(test_vertex([high_apex_coordinate; D]));
let low_simplex = (0..=D).collect();
let mut high_simplex: Vec<usize> = (1..=D).collect();
high_simplex.push(D + 1);
let simplices = vec![low_simplex, high_simplex];
DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices)
.unwrap()
.topology_guarantee(TopologyGuarantee::Pseudomanifold)
.build_triangulation()
.unwrap()
}
fn assert_randomized_full_report_matches_brute_force<const D: usize>()
where
AdaptiveKernel<f64>: ExactPredicates<D>,
{
for seed in [0x483, 0x483_0001, 0x483_0002] {
let triangulation = randomized_delaunay::<D>(seed);
assert!(triangulation.verify_via_flip_predicates().is_ok());
let optimized = triangulation.delaunay_violation_report(None).unwrap();
let tds = triangulation.into_triangulation().into_tds();
let brute_force = tds_delaunay_violation_report(&tds, None).unwrap();
assert_eq!(optimized, brute_force);
}
}
fn assert_adversarial_full_report_matches_brute_force<const D: usize>()
where
AdaptiveKernel<f64>: ExactPredicates<D>,
{
let triangulation = shared_facet_flip_adversary::<D>();
assert!(
triangulation
.delaunay_violation_report(None)
.is_ok_and(|report| !report.is_valid())
);
let optimized = triangulation.delaunay_violation_report(None).unwrap();
let brute_force = tds_delaunay_violation_report(triangulation.tds(), None).unwrap();
assert_eq!(optimized, brute_force);
}
macro_rules! generate_full_report_agreement_tests {
($dimension:literal) => {
pastey::paste! {
#[test]
fn [<randomized_full_report_matches_brute_force_ $dimension d>]() {
assert_randomized_full_report_matches_brute_force::<$dimension>();
}
#[test]
fn [<adversarial_full_report_matches_brute_force_ $dimension d>]() {
assert_adversarial_full_report_matches_brute_force::<$dimension>();
}
}
};
}
generate_full_report_agreement_tests!(2);
generate_full_report_agreement_tests!(3);
generate_full_report_agreement_tests!(4);
generate_full_report_agreement_tests!(5);
#[test]
fn complete_point_set_report_uses_robust_predicates_instead_of_owner_kernel() {
let source = randomized_delaunay::<2>(0x483_1001);
assert_eq!(
source.euclidean_report_domain,
EuclideanDelaunayReportDomain::CompletePointSet
);
let topology_guarantee = source.topology_guarantee();
let global_topology = source.global_topology();
let source_tds = source.into_triangulation().into_tds();
let mut triangulation = DelaunayTriangulationDraft::assemble_unchecked_for_test(
source_tds,
PanickingKernel,
topology_guarantee,
global_topology,
)
.into_unproven_delaunay_for_test();
triangulation.euclidean_report_domain = EuclideanDelaunayReportDomain::CompletePointSet;
let report = triangulation.delaunay_violation_report(None).unwrap();
let tds = triangulation.into_triangulation().into_tds();
let brute_force = tds_delaunay_violation_report(&tds, None).unwrap();
assert_eq!(report, brute_force);
}
#[test]
fn failed_complete_point_set_certificate_falls_back_to_global_report() {
let triangulation = shared_facet_flip_adversary::<2>();
let mut triangulation = DelaunayTriangulationDraft::from_triangulation(triangulation)
.into_unproven_delaunay_for_test();
triangulation.euclidean_report_domain = EuclideanDelaunayReportDomain::CompletePointSet;
let report = triangulation.delaunay_violation_report(None).unwrap();
let tds = triangulation.into_triangulation().into_tds();
let brute_force = tds_delaunay_violation_report(&tds, None).unwrap();
assert!(!report.is_valid());
assert_eq!(report, brute_force);
}
#[test]
fn unproven_connectivity_bypasses_local_certificate() {
let tds = tds_from_2d_vertices_and_simplices(
&[[0.0, 0.0], [2.0, 0.0], [0.0, 2.0], [0.25, 0.25]],
&[vec![0, 1, 2]],
);
assert!(verify_complete_euclidean_tds_via_robust_flip_predicates(&tds).is_ok());
let triangulation = DelaunayTriangulationDraft::assemble_unchecked_for_test(
tds,
PanickingKernel,
TopologyGuarantee::Pseudomanifold,
GlobalTopology::Euclidean,
)
.into_unproven_delaunay_for_test();
assert_eq!(
triangulation.euclidean_report_domain,
EuclideanDelaunayReportDomain::Unproven
);
assert_matches!(
triangulation.is_valid_delaunay(),
Err(DelaunayTriangulationValidationError::VerificationFailed { source })
if matches!(
source.as_ref(),
DelaunayVerificationError::EmptyCircumsphere { source }
if matches!(
source.as_ref(),
DelaunayValidationError::DelaunayViolation { .. }
)
)
);
let report = triangulation.delaunay_violation_report(None).unwrap();
let tds = triangulation.into_triangulation().into_tds();
let brute_force = tds_delaunay_violation_report(&tds, None).unwrap();
assert!(!report.is_valid());
assert_eq!(report, brute_force);
}
#[test]
fn complete_point_set_nonflippable_pyramid_matches_global_report() {
let vertices = [
test_vertex([-1.0, 0.0, 0.0]),
test_vertex([1.0, 0.0, 0.0]),
test_vertex([0.0, 0.0, 1.0]),
test_vertex([0.0, 1.0, 0.0]),
test_vertex([0.0, -0.5, 0.0]),
];
let simplices = [vec![0, 1, 2, 3], vec![0, 2, 1, 4]];
let tri =
DelaunayTriangulationBuilder::try_from_vertices_and_simplices(&vertices, &simplices)
.unwrap()
.build_triangulation()
.unwrap();
tri.validate_realization().unwrap();
let mut dt =
DelaunayTriangulationDraft::from_triangulation(tri).into_unproven_delaunay_for_test();
dt.euclidean_report_domain = EuclideanDelaunayReportDomain::CompletePointSet;
let global = tds_delaunay_violation_report(&dt.tri.tds, None).unwrap();
assert!(!global.is_valid());
assert_eq!(dt.delaunay_violation_report(None).unwrap(), global);
assert!(dt.is_valid_delaunay().is_err());
assert!(dt.delaunay_report().is_err());
}
#[test]
fn flip_predicate_certificate_cannot_publish_euclidean_topology() {
let vertices = [
test_vertex([0.0, 0.0]),
test_vertex([1.0, 0.0]),
test_vertex([0.0, 1.0]),
];
let triangulation = DelaunayTriangulationBuilder::new(&vertices)
.build_triangulation()
.unwrap();
verify_tds_via_flip_predicates_assuming_connected(
&triangulation.tds,
&triangulation.kernel,
triangulation.global_topology,
)
.unwrap();
let flip_certificate = DelaunayLevelFiveCertificate::for_triangulation(
&triangulation,
DelaunayLevelFiveEvidence::FlipPredicates,
);
assert!(!flip_certificate.applies_to(&triangulation));
let publication_certificate = certify_level_five_for_refinement(&triangulation).unwrap();
assert!(publication_certificate.applies_to(&triangulation));
}
#[test]
fn validation_cadence_maps_optional_every() {
assert_eq!(
ValidationCadence::from_optional_every(None),
ValidationCadence::Never
);
assert_eq!(
ValidationCadence::from_optional_every(Some(0)),
ValidationCadence::Never
);
assert_eq!(
ValidationCadence::from_optional_every(Some(128)),
ValidationCadence::EveryN(NonZeroUsize::new(128).unwrap())
);
}
#[test]
fn validation_cadence_should_validate_on_multiples() {
let cadence = ValidationCadence::EveryN(NonZeroUsize::new(64).unwrap());
assert!(!cadence.should_validate(0));
assert!(!cadence.should_validate(63));
assert!(cadence.should_validate(64));
assert!(!cadence.should_validate(65));
assert!(cadence.should_validate(128));
assert!(!ValidationCadence::Never.should_validate(64));
}
#[test]
fn verification_failed_display_includes_context() {
let err = DelaunayTriangulationValidationError::VerificationFailed {
source: synthetic_flip_verification_source(
"flip predicate detected non-Delaunay facet",
)
.into(),
};
let msg = err.to_string();
assert!(
msg.contains("Delaunay verification failed"),
"Display should contain prefix: {msg}"
);
assert!(
msg.contains("repair pass disconnected the triangulation"),
"Display should contain inner message: {msg}"
);
let DelaunayTriangulationValidationError::VerificationFailed { source } = &err else {
panic!("expected typed flip-predicate verification source, got {err:?}");
};
assert_matches!(
source.as_ref(),
DelaunayVerificationError::FlipPredicates { source }
if matches!(
source.as_ref(),
DelaunayRepairError::PostconditionFailed { .. }
)
);
}
#[test]
fn verification_error_kind_covers_empty_circumsphere_source() {
let simplex_key = SimplexKey::default();
let source = DelaunayVerificationError::from(DelaunayValidationError::DelaunayViolation {
simplex_key,
simplex_vertices: Box::default(),
offending_vertex: None,
neighbor_simplices: Box::default(),
});
assert_eq!(
DelaunayVerificationErrorKind::from(&source),
DelaunayVerificationErrorKind::EmptyCircumsphere,
);
assert!(source.to_string().contains("empty-circumsphere"));
let DelaunayVerificationError::EmptyCircumsphere { source } = source else {
panic!("expected empty-circumsphere source");
};
assert_matches!(
source.as_ref(),
DelaunayValidationError::DelaunayViolation {
simplex_key: actual,
..
} if *actual == simplex_key
);
}
#[test]
fn repair_operation_failed_preserves_source() {
let source = DelaunayRepairError::NonConvergent {
max_flips: 7,
diagnostics: Box::new(DelaunayRepairDiagnostics {
facets_checked: 3,
flips_performed: 7,
max_queue_len: 5,
ambiguous_predicates: 0,
ambiguous_predicate_samples: Vec::new(),
predicate_failures: 0,
cycle_detections: 0,
cycle_signature_samples: Vec::new(),
attempt: 1,
queue_order: RepairQueueOrder::Fifo,
}),
};
let err = DelaunayTriangulationValidationError::RepairOperationFailed {
operation: DelaunayRepairOperation::VertexRemoval,
source: Box::new(source),
};
let msg = err.to_string();
assert!(msg.contains("vertex removal"));
match &err {
DelaunayTriangulationValidationError::RepairOperationFailed {
operation: DelaunayRepairOperation::VertexRemoval,
source,
} if matches!(
source.as_ref(),
DelaunayRepairError::NonConvergent { max_flips: 7, .. }
) => {}
other => panic!("expected typed vertex-removal repair source, got {other:?}"),
}
let chained = err
.source()
.expect("typed repair failure should expose source error")
.to_string();
assert!(chained.contains("failed to converge after 7 flips"));
}
#[test]
fn tds_variant_display_delegates_to_source() {
let inner = TdsError::InconsistentDataStructure {
message: "broken link".to_string(),
};
let err = DelaunayTriangulationValidationError::from(inner);
assert!(err.to_string().contains("broken link"));
}
#[test]
fn triangulation_variant_display_delegates_to_source() {
let inner = TriangulationValidationError::IsolatedVertex {
vertex_key: VertexKey::from(KeyData::from_ffi(1)),
vertex_uuid: Uuid::nil(),
};
let err = DelaunayTriangulationValidationError::from(inner);
assert!(err.to_string().contains("Isolated vertex"));
}
#[test]
fn try_from_tds_rejects_non_delaunay_connectivity() {
init_tracing();
let tds = non_delaunay_quad_tds();
let err = DelaunayTriangulation::try_restore_from_tds_with_topology_context(
tds,
AdaptiveKernel::new(),
TopologyGuarantee::DEFAULT,
GlobalTopology::DEFAULT,
)
.expect_err("checked TDS reconstruction must reject non-Delaunay connectivity");
let failure = match err {
DelaunayTdsRestorationError::Delaunay { failure } => failure,
other => panic!("expected Level 5 refinement failure, got {other:?}"),
};
let (triangulation, reason) = failure.into_parts();
assert!(matches!(
reason,
DelaunayTriangulationValidationError::VerificationFailed { .. }
));
triangulation
.validate_realization()
.expect("failed Level 5 refinement must return the valid Levels 1-4 owner");
}
#[test]
fn report_domain_cannot_select_local_level_five_publication() {
let triangulation = shared_facet_flip_adversary::<2>();
let mut draft = DelaunayTriangulationDraft::from_triangulation(triangulation);
draft.set_euclidean_report_domain_for_test(EuclideanDelaunayReportDomain::CompletePointSet);
let failure = draft
.try_into_delaunay()
.expect_err("the adversarial shared facet must fail global Level 5 verification");
let (triangulation, reason) = failure.into_parts();
assert_matches!(
reason,
DelaunayTriangulationValidationError::VerificationFailed { source }
if matches!(
source.as_ref(),
DelaunayVerificationError::EmptyCircumsphere { .. }
)
);
triangulation
.validate_realization()
.expect("failed Level 5 promotion must return the valid Levels 1-4 owner");
}
#[test]
fn tds_audit_rejects_structural_corruption_before_proof_consumption() {
init_tracing();
let vertices = [
test_vertex([0.0, 0.0, 0.0]),
test_vertex([1.0, 0.0, 0.0]),
test_vertex([0.0, 1.0, 0.0]),
test_vertex([0.0, 0.0, 1.0]),
];
let dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let mut tds = dt.into_triangulation().into_tds();
let vk = tds.vertex_keys().next().unwrap();
let uuid = tds.vertex(vk).unwrap().uuid();
tds.remove_vertex_uuid_mapping_for_test(&uuid);
let err = tds
.validate()
.expect_err("the Levels 1–2 owner audit must reject broken UUID mappings");
assert_matches!(err, TdsError::MappingInconsistency { .. });
}
#[test]
fn try_from_tds_rejects_topology_validation_failure() {
init_tracing();
let err = DelaunayTriangulation::try_restore_from_tds_with_topology_context(
isolated_vertex_tds_3d(),
AdaptiveKernel::new(),
TopologyGuarantee::DEFAULT,
GlobalTopology::DEFAULT,
)
.expect_err("checked TDS reconstruction must reject isolated vertices");
let failure = match err {
DelaunayTdsRestorationError::Triangulation { failure } => failure,
other => panic!("expected Levels 3-4 refinement failure, got {other:?}"),
};
let (tds, reason) = failure.into_parts();
assert_matches!(
reason,
TriangulationBuilderError::TopologyValidation { source }
if matches!(
source.as_ref(),
InvariantError::Triangulation {
source: TriangulationValidationError::IsolatedVertex { .. }
}
)
);
tds.validate()
.expect("failed Levels 3-4 refinement must return the valid Levels 1-2 owner");
}
#[test]
fn restoration_reason_preserves_topology_failure_without_owner() {
let err = DelaunayTriangulation::try_restore_from_tds_with_topology_context(
isolated_vertex_tds_3d(),
AdaptiveKernel::new(),
TopologyGuarantee::DEFAULT,
GlobalTopology::DEFAULT,
)
.expect_err("checked TDS reconstruction must reject isolated vertices");
assert_matches!(
err.into_reason(),
DelaunayTdsRestorationReason::Triangulation {
source: TriangulationBuilderError::TopologyValidation { source }
} if matches!(
source.as_ref(),
InvariantError::Triangulation {
source: TriangulationValidationError::IsolatedVertex { .. }
}
)
);
}
#[test]
fn test_validation_report_ok_for_valid_triangulation() {
init_tracing();
let vertices = [
test_vertex([0.0, 0.0, 0.0]),
test_vertex([1.0, 0.0, 0.0]),
test_vertex([0.0, 1.0, 0.0]),
test_vertex([0.0, 0.0, 1.0]),
];
let dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
assert!(dt.validation_report().is_ok());
}
#[test]
fn test_validation_report_returns_mapping_failures_only() {
init_tracing();
let vertices = [
test_vertex([0.0, 0.0, 0.0]),
test_vertex([1.0, 0.0, 0.0]),
test_vertex([0.0, 1.0, 0.0]),
test_vertex([0.0, 0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let uuid = dt.tri.tds.vertices().next().unwrap().1.uuid();
dt.tri.tds.remove_vertex_uuid_mapping_for_test(&uuid);
let report = dt.validation_report().unwrap_err();
assert!(!report.violations.is_empty());
assert!(report.violations.iter().all(|v| {
matches!(
v.kind,
InvariantKind::VertexMappings | InvariantKind::SimplexMappings
)
}));
assert!(
report
.violations
.iter()
.all(|v| v.kind != InvariantKind::DelaunayProperty)
);
}
#[test]
fn test_validation_report_includes_vertex_incidence_violation() {
init_tracing();
let vertices = [
test_vertex([0.0, 0.0, 0.0]),
test_vertex([1.0, 0.0, 0.0]),
test_vertex([0.0, 1.0, 0.0]),
test_vertex([0.0, 0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let vertex_key = dt.tri.tds.vertices().next().unwrap().0;
dt.tri
.tds
.vertex_mut_for_test(vertex_key)
.unwrap()
.set_incident_simplex(Some(SimplexKey::default()));
let report = dt.validation_report().unwrap_err();
assert!(
report
.violations
.iter()
.any(|v| v.kind == InvariantKind::VertexIncidence)
);
}
#[test]
fn validation_report_includes_delaunay_after_realization_violations() {
init_tracing();
let tds = tds_from_2d_vertices_and_simplices(
&[[0.0, 0.0], [2.0, 0.0], [0.0, 2.0], [2.0, 2.0], [1.0, -1.0]],
&[vec![0, 1, 2], vec![2, 1, 3], vec![3, 2, 4]],
);
let dt = unchecked_test_delaunay_from_tds(tds);
let report = dt.validation_report().unwrap_err();
assert!(
report
.violations
.iter()
.any(|v| v.kind == InvariantKind::Realization),
"expected realization violation in report: {report:?}"
);
assert!(
report
.violations
.iter()
.any(|v| v.kind == InvariantKind::DelaunayProperty),
"expected Delaunay violation in report: {report:?}"
);
}
#[test]
fn test_dt_validate_maps_tds_error_to_tds_variant() {
init_tracing();
let vertices = [
test_vertex([0.0, 0.0, 0.0]),
test_vertex([1.0, 0.0, 0.0]),
test_vertex([0.0, 1.0, 0.0]),
test_vertex([0.0, 0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let vk = dt.vertices().next().unwrap().0;
let uuid = dt.vertex(vk).unwrap().uuid();
dt.tds_mut_for_repair()
.remove_vertex_uuid_mapping_for_test(&uuid);
match dt.validate() {
Err(DelaunayTriangulationValidationError::Tds { source })
if matches!(source.as_ref(), TdsError::MappingInconsistency { .. }) => {}
other => panic!(
"Expected DelaunayTriangulationValidationError::Tds with MappingInconsistency source, got {other:?}"
),
}
}
#[test]
fn test_dt_validate_maps_topology_error_to_triangulation_variant() {
init_tracing();
let vertices = [
test_vertex([0.0, 0.0, 0.0]),
test_vertex([1.0, 0.0, 0.0]),
test_vertex([0.0, 1.0, 0.0]),
test_vertex([0.0, 0.0, 1.0]),
];
let mut dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let _ = dt
.tds_mut_for_repair()
.insert_vertex_with_mapping(test_vertex([0.5, 0.5, 0.5]))
.unwrap();
match dt.validate() {
Err(DelaunayTriangulationValidationError::Triangulation { source })
if matches!(
source.as_ref(),
TriangulationValidationError::IsolatedVertex { .. }
) => {}
other => panic!(
"Expected DelaunayTriangulationValidationError::Triangulation with IsolatedVertex source, got {other:?}"
),
}
}
}
#[cfg(test)]
mod summary_tests {
use super::*;
use crate::core::algorithms::flips::DelaunayRepairPostconditionFailure;
use crate::topology::traits::topological_space::TopologyKind;
fn synthetic_delaunay_verification_error() -> DelaunayTriangulationValidationError {
DelaunayTriangulationValidationError::VerificationFailed {
source: DelaunayVerificationError::from(DelaunayRepairError::PostconditionFailed {
reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected {
simplex_count: 1,
}),
})
.into(),
}
}
#[test]
fn delaunay_validation_error_kind_from_error_preserves_all_variants() {
let cases = [
(
DelaunayTriangulationValidationError::from(TdsError::InconsistentDataStructure {
message: "dangling simplex".to_string(),
}),
DelaunayValidationErrorKind::Tds,
),
(
DelaunayTriangulationValidationError::from(
TriangulationValidationError::Disconnected { simplex_count: 2 },
),
DelaunayValidationErrorKind::Triangulation,
),
(
DelaunayTriangulationValidationError::Realization {
source: Box::new(
TriangulationRealizationValidationError::UnsupportedTopology {
topology: TopologyKind::Hyperbolic,
dimension: 2,
},
),
},
DelaunayValidationErrorKind::Realization,
),
(
synthetic_delaunay_verification_error(),
DelaunayValidationErrorKind::VerificationFailed,
),
(
DelaunayTriangulationValidationError::RepairOperationFailed {
operation: DelaunayRepairOperation::VertexRemoval,
source: Box::new(DelaunayRepairError::PostconditionFailed {
reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected {
simplex_count: 1,
}),
}),
},
DelaunayValidationErrorKind::RepairOperationFailed,
),
];
for (source, expected) in cases {
assert_eq!(DelaunayValidationErrorKind::from(&source), expected);
}
}
}