#![forbid(unsafe_code)]
use super::{
AllFacetsIter, BistellarFlipKind, DataType, DelaunayRepairError,
DelaunayRepairPostconditionFailure, DelaunayRepairVerificationContext, Duration, EdgeKey,
FacetHandle, FastHashMap, FastHashSet, FlipDirection, FlipError, FlipSignature, GlobalTopology,
GlobalTopologyModelAdapter, Instant, Kernel, LastAppliedFlip, MAX_REPEAT_SIGNATURE,
RepairAttemptConfig, RepairDiagnostics, RepairQueueOrder, RepairQueues, RidgeHandle,
RobustKernel, SimplexKey, SimplexKeyBuffer, Tds, TdsRollbackTransaction, TdsRollbackWindow,
TopologicalOperation, TopologyGuarantee, TriangleHandle, VecDeque, VertexKey,
apply_delaunay_flip_k2, build_k2_flip_context, build_k2_flip_context_from_edge,
build_k3_flip_context, build_k3_flip_context_from_triangle, debug_postcondition_facet_context,
debug_ridge_context, default_max_flips, delaunay_violation_k2_for_facet,
delaunay_violation_k3_for_ridge, duration_nanos_saturating, emit_repair_debug_summary,
enqueue_facet, enqueue_simplex_facets, env, facet_key_from_vertices,
facet_vertices_from_simplex, flip_signature, flip_would_create_degenerate_simplex,
is_delaunay_violation_k2, is_delaunay_violation_k3, k2_flip_would_create_degenerate_simplex,
non_convergent_error, pop_queue, removed_simplex_frame, repair_flip_is_unavailable,
repair_ridge_debug_enabled, repair_trace_enabled, ridge_vertices_from_simplex,
run_next_edge_repair_step, run_next_facet_repair_step, run_next_ridge_repair_step,
run_next_triangle_repair_step, seed_repair_queues, validate_bistellar_flip_dynamic,
};
pub(super) fn repair_delaunay_with_flips_k2_k3_attempt<K, U, V, const D: usize>(
tds: &mut Tds<U, V, D>,
kernel: &K,
seed_simplices: Option<&[SimplexKey]>,
global_topology: GlobalTopology<D>,
config: &RepairAttemptConfig,
) -> Result<RepairAttemptOutcome, DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
{
repair_delaunay_with_flips_k2_k3_attempt_timed(
tds,
kernel,
seed_simplices,
global_topology,
config,
None,
)
}
#[expect(
clippy::too_many_lines,
reason = "Repair loop contains inline tracing and queue handling for diagnostics"
)]
pub(super) fn repair_delaunay_with_flips_k2_k3_attempt_timed<K, U, V, const D: usize>(
tds: &mut Tds<U, V, D>,
kernel: &K,
seed_simplices: Option<&[SimplexKey]>,
global_topology: GlobalTopology<D>,
config: &RepairAttemptConfig,
mut timing: Option<&mut LocalRepairPhaseTiming>,
) -> Result<RepairAttemptOutcome, DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
{
if D < 2 {
return Err(FlipError::UnsupportedDimension { dimension: D }.into());
}
let topology_model = global_topology.model();
if D == 2 {
return repair_delaunay_with_flips_k2_attempt(
tds,
kernel,
seed_simplices,
&topology_model,
config,
);
}
let max_flips = config
.max_flips_override
.unwrap_or_else(|| default_max_flips::<D>(tds.number_of_simplices()));
let mut stats = DelaunayRepairStats::default();
let mut diagnostics = RepairDiagnostics::default();
let mut queues = RepairQueues::new();
let mut last_applied_flip: Option<LastAppliedFlip> = None;
let seed_started = timing.is_some().then(Instant::now);
let used_full_reseed = seed_repair_queues(tds, seed_simplices, &mut queues, &mut stats)?;
if let (Some(timing), Some(seed_started)) = (timing.as_deref_mut(), seed_started) {
timing.record_attempt_seed(seed_started.elapsed());
}
let mut touched_simplices = SimplexKeyBuffer::new();
let mut touched_simplex_set = FastHashSet::<SimplexKey>::default();
let mut prefer_secondary = false;
macro_rules! timed_step {
($recorder:ident, $step:expr) => {{
if timing.is_some() {
let started = Instant::now();
let processed = $step?;
if let Some(timing) = timing.as_deref_mut() {
timing.$recorder(started.elapsed());
}
processed
} else {
$step?
}
}};
}
while queues.has_work() {
if prefer_secondary {
let processed_ridge = timed_step!(
record_attempt_ridge,
run_next_ridge_repair_step(
tds,
kernel,
&topology_model,
&mut queues,
&mut stats,
max_flips,
config,
&mut diagnostics,
&mut last_applied_flip,
&mut touched_simplices,
&mut touched_simplex_set,
)
);
let processed_edge = !processed_ridge
&& timed_step!(
record_attempt_edge,
run_next_edge_repair_step(
tds,
kernel,
&topology_model,
&mut queues,
&mut stats,
max_flips,
config,
&mut diagnostics,
&mut last_applied_flip,
&mut touched_simplices,
&mut touched_simplex_set,
)
);
let processed_triangle = !processed_ridge
&& !processed_edge
&& timed_step!(
record_attempt_triangle,
run_next_triangle_repair_step(
tds,
kernel,
&topology_model,
&mut queues,
&mut stats,
max_flips,
config,
&mut diagnostics,
&mut last_applied_flip,
&mut touched_simplices,
&mut touched_simplex_set,
)
);
if processed_ridge || processed_edge || processed_triangle {
prefer_secondary = false;
continue;
}
}
if timed_step!(
record_attempt_facet,
run_next_facet_repair_step(
tds,
kernel,
&topology_model,
&mut queues,
&mut stats,
max_flips,
config,
&mut diagnostics,
&mut last_applied_flip,
&mut touched_simplices,
&mut touched_simplex_set,
)
) {
prefer_secondary = true;
continue;
}
let processed_ridge = timed_step!(
record_attempt_ridge,
run_next_ridge_repair_step(
tds,
kernel,
&topology_model,
&mut queues,
&mut stats,
max_flips,
config,
&mut diagnostics,
&mut last_applied_flip,
&mut touched_simplices,
&mut touched_simplex_set,
)
);
let processed_edge = !processed_ridge
&& timed_step!(
record_attempt_edge,
run_next_edge_repair_step(
tds,
kernel,
&topology_model,
&mut queues,
&mut stats,
max_flips,
config,
&mut diagnostics,
&mut last_applied_flip,
&mut touched_simplices,
&mut touched_simplex_set,
)
);
let processed_triangle = !processed_ridge
&& !processed_edge
&& timed_step!(
record_attempt_triangle,
run_next_triangle_repair_step(
tds,
kernel,
&topology_model,
&mut queues,
&mut stats,
max_flips,
config,
&mut diagnostics,
&mut last_applied_flip,
&mut touched_simplices,
&mut touched_simplex_set,
)
);
if processed_ridge || processed_edge || processed_triangle {
prefer_secondary = false;
}
}
if repair_trace_enabled() {
tracing::debug!(
"[repair] attempt={} done: checked={} flips={} max_queue={} ambiguous={} predicate_failures={} cycles={}",
config.attempt,
stats.facets_checked,
stats.flips_performed,
stats.max_queue_len,
diagnostics.ambiguous_predicates,
diagnostics.predicate_failures,
diagnostics.cycle_detections,
);
}
emit_repair_debug_summary("attempt_done", &stats, &diagnostics, config, max_flips);
Ok(RepairAttemptOutcome {
postcondition_required: repair_postcondition_required(&stats, &diagnostics),
stats,
last_applied_flip,
touched_simplices,
used_full_reseed,
})
}
#[derive(Debug, Clone, Copy)]
pub(super) struct FlipCycleContext<'a> {
signature: FlipSignature,
kind: BistellarFlipKind,
direction: FlipDirection,
removed_face_vertices: &'a [VertexKey],
inserted_face_vertices: &'a [VertexKey],
}
impl<'a> FlipCycleContext<'a> {
pub(super) const fn from_validated_flip(
signature: FlipSignature,
kind: BistellarFlipKind,
direction: FlipDirection,
removed_face_vertices: &'a [VertexKey],
inserted_face_vertices: &'a [VertexKey],
) -> Self {
Self {
signature,
kind,
direction,
removed_face_vertices,
inserted_face_vertices,
}
}
}
pub(super) fn check_flip_cycle<U, V, const D: usize>(
tds: &Tds<U, V, D>,
context: FlipCycleContext<'_>,
diagnostics: &mut RepairDiagnostics,
stats: &DelaunayRepairStats,
max_flips: usize,
config: &RepairAttemptConfig,
) -> Result<(), DelaunayRepairError>
where
U: DataType,
V: DataType,
{
let repeats = diagnostics
.flip_signature_counts
.get(&context.signature)
.copied()
.unwrap_or(0);
if repeats >= MAX_REPEAT_SIGNATURE {
if repair_trace_enabled() {
let removed_details: Vec<_> = context
.removed_face_vertices
.iter()
.filter_map(|&vkey| tds.vertex(vkey).map(|v| (vkey, *v.point())))
.collect();
let inserted_details: Vec<_> = context
.inserted_face_vertices
.iter()
.filter_map(|&vkey| tds.vertex(vkey).map(|v| (vkey, *v.point())))
.collect();
tracing::debug!(
"[repair] cycle abort signature={} repeats={} flips={} max_flips={} attempt={} order={:?} k={} direction={:?} removed_face={:?} inserted_face={:?}",
context.signature,
repeats,
stats.flips_performed,
max_flips,
config.attempt,
config.queue_order,
context.kind.k(),
context.direction,
removed_details,
inserted_details,
);
}
diagnostics.record_cycle_abort(context.signature);
return Err(non_convergent_error(max_flips, stats, diagnostics, config));
}
Ok(())
}
pub(super) fn resolve_facet_handle_for_key<U, V, const D: usize>(
tds: &Tds<U, V, D>,
handle: FacetHandle,
key: u64,
) -> Option<FacetHandle>
where
U: DataType,
V: DataType,
{
let simplex_key = handle.simplex_key();
let simplex = tds.simplex(simplex_key)?;
let facet_index = usize::from(handle.facet_index());
if facet_index < simplex.number_of_vertices() {
let facet_vertices = facet_vertices_from_simplex(simplex, facet_index);
if facet_key_from_vertices(&facet_vertices) == key {
return Some(handle);
}
}
for candidate_idx in 0..simplex.number_of_vertices() {
let facet_vertices = facet_vertices_from_simplex(simplex, candidate_idx);
if facet_key_from_vertices(&facet_vertices) == key {
let facet_index = u8::try_from(candidate_idx).ok()?;
return Some(FacetHandle::from_validated(simplex_key, facet_index));
}
}
None
}
pub(super) fn resolve_ridge_handle_for_key<U, V, const D: usize>(
tds: &Tds<U, V, D>,
handle: RidgeHandle,
key: u64,
) -> Option<RidgeHandle>
where
U: DataType,
V: DataType,
{
if D < 3 {
return None;
}
let simplex_key = handle.simplex_key();
let simplex = tds.simplex(simplex_key)?;
let vertex_count = simplex.number_of_vertices();
let omit_a = usize::from(handle.omit_a());
let omit_b = usize::from(handle.omit_b());
if omit_a < vertex_count && omit_b < vertex_count && omit_a != omit_b {
let ridge_vertices = ridge_vertices_from_simplex(simplex, omit_a, omit_b);
if ridge_vertices.len() == D - 1 && facet_key_from_vertices(&ridge_vertices) == key {
return Some(handle);
}
}
for i in 0..vertex_count {
for j in (i + 1)..vertex_count {
let ridge_vertices = ridge_vertices_from_simplex(simplex, i, j);
if ridge_vertices.len() != D - 1 {
continue;
}
if facet_key_from_vertices(&ridge_vertices) == key {
let omit_a = u8::try_from(i).ok()?;
let omit_b = u8::try_from(j).ok()?;
return Some(RidgeHandle::from_validated(simplex_key, omit_a, omit_b));
}
}
}
None
}
#[derive(Debug, Clone, Default)]
pub struct DelaunayRepairStats {
pub facets_checked: usize,
pub flips_performed: usize,
pub max_queue_len: usize,
}
#[expect(
clippy::struct_field_names,
reason = "phase timing telemetry keeps units explicit on every exported field"
)]
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct LocalRepairPhaseTiming {
pub(crate) snapshot_nanos: u64,
pub(crate) attempt_nanos: u64,
pub(crate) attempt_seed_nanos: u64,
pub(crate) attempt_facet_nanos: u64,
pub(crate) attempt_ridge_nanos: u64,
pub(crate) attempt_edge_nanos: u64,
pub(crate) attempt_triangle_nanos: u64,
pub(crate) postcondition_nanos: u64,
pub(crate) restore_nanos: u64,
}
impl LocalRepairPhaseTiming {
fn record_snapshot(&mut self, elapsed: Duration) {
self.snapshot_nanos = self
.snapshot_nanos
.saturating_add(duration_nanos_saturating(elapsed));
}
fn record_attempt(&mut self, elapsed: Duration) {
self.attempt_nanos = self
.attempt_nanos
.saturating_add(duration_nanos_saturating(elapsed));
}
fn record_attempt_seed(&mut self, elapsed: Duration) {
self.attempt_seed_nanos = self
.attempt_seed_nanos
.saturating_add(duration_nanos_saturating(elapsed));
}
fn record_attempt_facet(&mut self, elapsed: Duration) {
self.attempt_facet_nanos = self
.attempt_facet_nanos
.saturating_add(duration_nanos_saturating(elapsed));
}
fn record_attempt_ridge(&mut self, elapsed: Duration) {
self.attempt_ridge_nanos = self
.attempt_ridge_nanos
.saturating_add(duration_nanos_saturating(elapsed));
}
fn record_attempt_edge(&mut self, elapsed: Duration) {
self.attempt_edge_nanos = self
.attempt_edge_nanos
.saturating_add(duration_nanos_saturating(elapsed));
}
fn record_attempt_triangle(&mut self, elapsed: Duration) {
self.attempt_triangle_nanos = self
.attempt_triangle_nanos
.saturating_add(duration_nanos_saturating(elapsed));
}
fn record_postcondition(&mut self, elapsed: Duration) {
self.postcondition_nanos = self
.postcondition_nanos
.saturating_add(duration_nanos_saturating(elapsed));
}
fn record_restore(&mut self, elapsed: Duration) {
self.restore_nanos = self
.restore_nanos
.saturating_add(duration_nanos_saturating(elapsed));
}
}
pub(super) fn publish_local_repair_phase_timing(
timing: &mut Option<&mut LocalRepairPhaseTiming>,
phase_timing: LocalRepairPhaseTiming,
) {
if let Some(timing) = timing.as_deref_mut() {
*timing = phase_timing;
}
}
#[derive(Debug, Clone)]
pub(crate) struct DelaunayRepairRun {
pub stats: DelaunayRepairStats,
pub touched_simplices: SimplexKeyBuffer,
pub used_full_reseed: bool,
}
#[derive(Debug)]
pub(super) struct RepairAttemptOutcome {
pub(super) postcondition_required: bool,
pub(super) stats: DelaunayRepairStats,
pub(super) last_applied_flip: Option<LastAppliedFlip>,
pub(super) touched_simplices: SimplexKeyBuffer,
pub(super) used_full_reseed: bool,
}
pub(super) const fn repair_postcondition_required(
stats: &DelaunayRepairStats,
diagnostics: &RepairDiagnostics,
) -> bool {
stats.flips_performed > 0 || diagnostics.saw_applicable_repair_site
}
pub(super) fn record_touched_simplices(
touched_simplices: &mut SimplexKeyBuffer,
touched_simplex_set: &mut FastHashSet<SimplexKey>,
new_simplices: &[SimplexKey],
) {
for &simplex_key in new_simplices {
if touched_simplex_set.insert(simplex_key) {
touched_simplices.push(simplex_key);
}
}
}
pub(super) fn local_postcondition_frontier(
seed_simplices: &[SimplexKey],
touched_simplices: &[SimplexKey],
) -> SimplexKeyBuffer {
let mut frontier = SimplexKeyBuffer::new();
let mut seen = FastHashSet::<SimplexKey>::default();
for &simplex_key in seed_simplices.iter().chain(touched_simplices) {
if seen.insert(simplex_key) {
frontier.push(simplex_key);
}
}
frontier
}
pub(super) fn repair_run_from_attempt(outcome: RepairAttemptOutcome) -> DelaunayRepairRun {
let RepairAttemptOutcome {
stats,
touched_simplices,
used_full_reseed,
..
} = outcome;
DelaunayRepairRun {
stats,
touched_simplices,
used_full_reseed,
}
}
#[expect(
clippy::too_many_lines,
reason = "Repair loop contains inline tracing and queue handling for diagnostics"
)]
pub(super) fn repair_delaunay_with_flips_k2_attempt<K, U, V, const D: usize>(
tds: &mut Tds<U, V, D>,
kernel: &K,
seed_simplices: Option<&[SimplexKey]>,
topology_model: &GlobalTopologyModelAdapter<D>,
config: &RepairAttemptConfig,
) -> Result<RepairAttemptOutcome, DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
{
if D < 2 {
return Err(FlipError::UnsupportedDimension { dimension: D }.into());
}
let max_flips = config
.max_flips_override
.unwrap_or_else(|| default_max_flips::<D>(tds.number_of_simplices()));
let mut stats = DelaunayRepairStats::default();
let mut diagnostics = RepairDiagnostics::default();
let mut queue: VecDeque<(FacetHandle, u64)> = VecDeque::new();
let mut queued: FastHashSet<u64> = FastHashSet::default();
let mut facet_handles: FastHashMap<u64, FacetHandle> = FastHashMap::default();
let mut last_applied_flip: Option<LastAppliedFlip> = None;
let mut touched_simplices = SimplexKeyBuffer::new();
let mut touched_simplex_set = FastHashSet::<SimplexKey>::default();
let used_full_reseed = seed_simplices.is_none();
if let Some(seeds) = seed_simplices {
for &simplex_key in seeds {
enqueue_simplex_facets(
tds,
simplex_key,
&mut queue,
&mut queued,
&mut facet_handles,
&mut stats,
)?;
}
} else {
for facet in AllFacetsIter::try_new(tds)? {
let facet = facet?;
let handle = FacetHandle::from_validated(facet.simplex_key(), facet.facet_index());
enqueue_facet(
tds,
handle,
&mut queue,
&mut queued,
&mut facet_handles,
&mut stats,
);
}
}
if repair_trace_enabled() {
let seed_count = seed_simplices.map_or(0, <[SimplexKey]>::len);
tracing::debug!(
"[repair] attempt={} order={:?} simplices={} max_flips={} seeds={} queues(facet={})",
config.attempt,
config.queue_order,
tds.number_of_simplices(),
max_flips,
seed_count,
queue.len(),
);
}
while let Some((facet, key)) = pop_queue(&mut queue, config.queue_order) {
queued.remove(&key);
let facet = facet_handles.remove(&key).unwrap_or(facet);
let Some(facet) = resolve_facet_handle_for_key(tds, facet, key) else {
continue;
};
stats.facets_checked += 1;
let context = match build_k2_flip_context(tds, facet) {
Ok(ctx) => ctx,
Err(
FlipError::BoundaryFacet { .. }
| FlipError::MissingSimplex { .. }
| FlipError::MissingNeighbor { .. }
| FlipError::InvalidFacetAdjacency { .. }
| FlipError::InvalidFacetIndex { .. },
) => {
continue;
}
Err(e) => return Err(e.into()),
};
let violates = match is_delaunay_violation_k2(
tds,
kernel,
topology_model,
&context,
config,
&mut diagnostics,
) {
Ok(violates) => violates,
Err(FlipError::PredicateFailure { .. }) => {
continue;
}
Err(e) => return Err(e.into()),
};
if !violates {
continue;
}
diagnostics.record_applicable_repair_site();
let kind = BistellarFlipKind::from_validated(2, D);
let signature = flip_signature(
kind,
context.direction,
&context.removed_face_vertices,
&context.inserted_face_vertices,
);
check_flip_cycle(
tds,
FlipCycleContext::from_validated_flip(
signature,
kind,
context.direction,
&context.removed_face_vertices,
&context.inserted_face_vertices,
),
&mut diagnostics,
&stats,
max_flips,
config,
)?;
if stats.flips_performed >= max_flips {
return Err(non_convergent_error(
max_flips,
&stats,
&diagnostics,
config,
));
}
let applied = match apply_delaunay_flip_k2(tds, &context) {
Ok(applied) => applied,
Err(err) if repair_flip_is_unavailable(&err) => {
if env::var_os("DELAUNAY_REPAIR_DEBUG_FACETS").is_some() {
tracing::debug!(
"k=2 flip skipped in repair_delaunay_with_flips_k2_attempt (facet={facet:?}): {err}"
);
}
if repair_trace_enabled() {
tracing::debug!("[repair] skip k=2 flip (facet={facet:?}) reason={err}");
tracing::debug!(
"[repair] skip k=2 flip context removed_face={:?} inserted_face={:?} removed_simplices={:?}",
context.removed_face_vertices,
context.inserted_face_vertices,
context.removed_simplices,
);
}
continue;
}
Err(e) => return Err(e.into()),
};
stats.flips_performed += 1;
diagnostics.record_flip_signature(signature);
last_applied_flip = Some(LastAppliedFlip::from_applied_flip(&applied));
let info = applied.info;
record_touched_simplices(
&mut touched_simplices,
&mut touched_simplex_set,
&info.new_simplices,
);
for &simplex_key in &info.new_simplices {
enqueue_simplex_facets(
tds,
simplex_key,
&mut queue,
&mut queued,
&mut facet_handles,
&mut stats,
)?;
}
}
if repair_trace_enabled() {
tracing::debug!(
"[repair] attempt={} done: checked={} flips={} max_queue={} ambiguous={} predicate_failures={} cycles={}",
config.attempt,
stats.facets_checked,
stats.flips_performed,
stats.max_queue_len,
diagnostics.ambiguous_predicates,
diagnostics.predicate_failures,
diagnostics.cycle_detections,
);
}
emit_repair_debug_summary("attempt_done", &stats, &diagnostics, config, max_flips);
Ok(RepairAttemptOutcome {
postcondition_required: repair_postcondition_required(&stats, &diagnostics),
stats,
last_applied_flip,
touched_simplices,
used_full_reseed,
})
}
pub(crate) fn repair_delaunay_with_flips_k2_k3<K, U, V, const D: usize>(
tds: &mut Tds<U, V, D>,
kernel: &K,
seed_simplices: Option<&[SimplexKey]>,
topology: TopologyGuarantee,
global_topology: GlobalTopology<D>,
max_flips_override: Option<usize>,
) -> Result<DelaunayRepairStats, DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
{
repair_delaunay_with_flips_k2_k3_run(
tds,
kernel,
seed_simplices,
topology,
global_topology,
max_flips_override,
)
.map(|run| run.stats)
}
pub(super) fn run_full_reseed_retry<K, U, V, W, const D: usize>(
transaction: &mut W,
kernel: &K,
global_topology: GlobalTopology<D>,
config: &RepairAttemptConfig,
) -> Result<DelaunayRepairRun, DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
W: TdsRollbackWindow<U, V, D>,
{
transaction.restore_rollback_tds();
let retry_seed_simplices = None;
let topology_model = global_topology.model();
let attempt_result = if D == 2 {
repair_delaunay_with_flips_k2_attempt(
transaction.rollback_tds_mut(),
kernel,
retry_seed_simplices,
&topology_model,
config,
)
} else {
repair_delaunay_with_flips_k2_k3_attempt(
transaction.rollback_tds_mut(),
kernel,
retry_seed_simplices,
global_topology,
config,
)
};
let outcome = attempt_result?;
verify_repair_postcondition_with_topology(
transaction.rollback_tds_mut(),
kernel,
retry_seed_simplices,
global_topology,
PostconditionMode::Repair,
outcome.last_applied_flip.as_ref(),
ConnectivityPostcondition::Check,
)?;
Ok(repair_run_from_attempt(outcome))
}
pub(crate) fn repair_delaunay_with_flips_k2_k3_run<K, U, V, const D: usize>(
tds: &mut Tds<U, V, D>,
kernel: &K,
seed_simplices: Option<&[SimplexKey]>,
topology: TopologyGuarantee,
global_topology: GlobalTopology<D>,
max_flips_override: Option<usize>,
) -> Result<DelaunayRepairRun, DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
{
let mut transaction = TdsRollbackTransaction::begin(tds);
match repair_delaunay_with_flips_k2_k3_run_in_transaction(
&mut transaction,
kernel,
seed_simplices,
topology,
global_topology,
max_flips_override,
) {
Ok(run) => {
transaction.commit();
Ok(run)
}
Err(error) => {
transaction.rollback();
Err(error)
}
}
}
pub(crate) fn repair_delaunay_with_flips_k2_k3_run_in_transaction<K, U, V, W, const D: usize>(
transaction: &mut W,
kernel: &K,
seed_simplices: Option<&[SimplexKey]>,
topology: TopologyGuarantee,
global_topology: GlobalTopology<D>,
max_flips_override: Option<usize>,
) -> Result<DelaunayRepairRun, DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
W: TdsRollbackWindow<U, V, D>,
{
if D < 2 {
return Err(FlipError::UnsupportedDimension { dimension: D }.into());
}
let operation = TopologicalOperation::FacetFlip;
if !operation.is_admissible_under(topology) {
return Err(DelaunayRepairError::InvalidTopology {
required: operation.required_topology(),
found: topology,
message: "flip-based Delaunay repair requires admissible topology",
});
}
let attempt1 = RepairAttemptConfig {
attempt: 1,
queue_order: RepairQueueOrder::Fifo,
max_flips_override,
};
let attempt2 = RepairAttemptConfig {
attempt: 2,
queue_order: RepairQueueOrder::Lifo,
max_flips_override,
};
let topology_model = global_topology.model();
let attempt1_result = if D == 2 {
repair_delaunay_with_flips_k2_attempt(
transaction.rollback_tds_mut(),
kernel,
seed_simplices,
&topology_model,
&attempt1,
)
} else {
repair_delaunay_with_flips_k2_k3_attempt(
transaction.rollback_tds_mut(),
kernel,
seed_simplices,
global_topology,
&attempt1,
)
};
match attempt1_result {
Ok(outcome) => {
if verify_repair_postcondition_with_topology(
transaction.rollback_tds_mut(),
kernel,
seed_simplices,
global_topology,
PostconditionMode::Repair,
outcome.last_applied_flip.as_ref(),
ConnectivityPostcondition::Check,
)
.is_ok()
{
let run = repair_run_from_attempt(outcome);
return Ok(run);
}
if repair_trace_enabled() {
tracing::debug!(
"[repair] attempt 1 postcondition failed; retrying with LIFO + full reseed"
);
}
}
Err(DelaunayRepairError::NonConvergent { .. }) => {
if repair_trace_enabled() {
tracing::debug!(
"[repair] attempt 1 non-convergent; retrying with LIFO + full reseed"
);
}
}
Err(err) => {
return Err(err);
}
}
run_full_reseed_retry(transaction, kernel, global_topology, &attempt2)
}
pub(crate) fn repair_delaunay_local_single_pass<K, U, V, const D: usize>(
tds: &mut Tds<U, V, D>,
kernel: &K,
seed_simplices: &[SimplexKey],
max_flips: usize,
) -> Result<DelaunayRepairStats, DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
{
repair_delaunay_local_single_pass_timed(tds, kernel, seed_simplices, max_flips, None)
}
#[expect(
clippy::too_many_lines,
reason = "bounded two-attempt repair keeps rollback, retry, and postcondition timing together"
)]
pub(crate) fn repair_delaunay_local_single_pass_timed<K, U, V, const D: usize>(
tds: &mut Tds<U, V, D>,
kernel: &K,
seed_simplices: &[SimplexKey],
max_flips: usize,
mut timing: Option<&mut LocalRepairPhaseTiming>,
) -> Result<DelaunayRepairStats, DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
{
let mut phase_timing = LocalRepairPhaseTiming::default();
let global_topology = GlobalTopology::DEFAULT;
let topology_model = global_topology.model();
let attempt1 = RepairAttemptConfig {
attempt: 1,
queue_order: RepairQueueOrder::Fifo,
max_flips_override: Some(max_flips),
};
let attempt2 = RepairAttemptConfig {
attempt: 2,
queue_order: RepairQueueOrder::Lifo,
max_flips_override: Some(max_flips),
};
let snapshot_started = Instant::now();
let mut transaction = TdsRollbackTransaction::begin(tds);
phase_timing.record_snapshot(snapshot_started.elapsed());
let attempt_started = Instant::now();
let attempt1_result = if D == 2 {
repair_delaunay_with_flips_k2_attempt(
transaction.tds_mut(),
kernel,
Some(seed_simplices),
&topology_model,
&attempt1,
)
} else {
repair_delaunay_with_flips_k2_k3_attempt_timed(
transaction.tds_mut(),
kernel,
Some(seed_simplices),
global_topology,
&attempt1,
Some(&mut phase_timing),
)
};
phase_timing.record_attempt(attempt_started.elapsed());
match attempt1_result {
Ok(outcome) => {
if !outcome.postcondition_required || D >= 4 {
let stats = outcome.stats;
transaction.commit();
publish_local_repair_phase_timing(&mut timing, phase_timing);
return Ok(stats);
}
let postcondition_frontier =
local_postcondition_frontier(seed_simplices, &outcome.touched_simplices);
let postcondition_started = Instant::now();
let postcondition_result = verify_local_repair_postcondition(
transaction.tds_mut(),
kernel,
&postcondition_frontier,
outcome.last_applied_flip.as_ref(),
);
phase_timing.record_postcondition(postcondition_started.elapsed());
if postcondition_result.is_ok() {
let stats = outcome.stats;
transaction.commit();
publish_local_repair_phase_timing(&mut timing, phase_timing);
return Ok(stats);
}
if repair_trace_enabled() {
tracing::debug!("[repair] local attempt 1 postcondition failed; retrying LIFO");
}
}
Err(DelaunayRepairError::NonConvergent { .. }) => {
if repair_trace_enabled() {
tracing::debug!("[repair] local attempt 1 non-convergent; retrying LIFO");
}
}
Err(err) => {
let restore_started = Instant::now();
transaction.rollback();
phase_timing.record_restore(restore_started.elapsed());
publish_local_repair_phase_timing(&mut timing, phase_timing);
return Err(err);
}
}
let restore_started = Instant::now();
transaction.restore();
phase_timing.record_restore(restore_started.elapsed());
let attempt_started = Instant::now();
let attempt2_result = if D == 2 {
repair_delaunay_with_flips_k2_attempt(
transaction.tds_mut(),
kernel,
Some(seed_simplices),
&topology_model,
&attempt2,
)
} else {
repair_delaunay_with_flips_k2_k3_attempt_timed(
transaction.tds_mut(),
kernel,
Some(seed_simplices),
global_topology,
&attempt2,
Some(&mut phase_timing),
)
};
phase_timing.record_attempt(attempt_started.elapsed());
match attempt2_result {
Ok(outcome) => {
if !outcome.postcondition_required || D >= 4 {
let stats = outcome.stats;
transaction.commit();
publish_local_repair_phase_timing(&mut timing, phase_timing);
return Ok(stats);
}
let postcondition_frontier =
local_postcondition_frontier(seed_simplices, &outcome.touched_simplices);
let postcondition_started = Instant::now();
let postcondition_result = verify_local_repair_postcondition(
transaction.tds_mut(),
kernel,
&postcondition_frontier,
outcome.last_applied_flip.as_ref(),
);
phase_timing.record_postcondition(postcondition_started.elapsed());
match postcondition_result {
Ok(()) => {
let stats = outcome.stats;
transaction.commit();
publish_local_repair_phase_timing(&mut timing, phase_timing);
Ok(stats)
}
Err(verifier_err) => {
let restore_started = Instant::now();
transaction.rollback();
phase_timing.record_restore(restore_started.elapsed());
publish_local_repair_phase_timing(&mut timing, phase_timing);
Err(verifier_err)
}
}
}
Err(err) => {
let restore_started = Instant::now();
transaction.rollback();
phase_timing.record_restore(restore_started.elapsed());
publish_local_repair_phase_timing(&mut timing, phase_timing);
Err(err)
}
}
}
pub(crate) fn verify_tds_via_flip_predicates_assuming_connected<K, U, V, const D: usize>(
tds: &Tds<U, V, D>,
kernel: &K,
global_topology: GlobalTopology<D>,
) -> Result<(), DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
{
verify_repair_postcondition_with_topology(
tds,
kernel,
None,
global_topology,
PostconditionMode::Strict,
None,
ConnectivityPostcondition::Defer,
)
}
pub(crate) fn verify_complete_euclidean_tds_via_robust_flip_predicates<U, V, const D: usize>(
tds: &Tds<U, V, D>,
) -> Result<(), DelaunayRepairError> {
verify_delaunay_with_topology(tds, &RobustKernel::new(), GlobalTopology::Euclidean)
}
pub(super) fn verify_delaunay_with_topology<K, U, V, const D: usize>(
tds: &Tds<U, V, D>,
kernel: &K,
global_topology: GlobalTopology<D>,
) -> Result<(), DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
{
verify_repair_postcondition_with_topology(
tds,
kernel,
None,
global_topology,
PostconditionMode::Strict,
None,
ConnectivityPostcondition::Check,
)
}
pub(super) fn verify_local_repair_postcondition<K, U, V, const D: usize>(
tds: &Tds<U, V, D>,
kernel: &K,
seed_simplices: &[SimplexKey],
last_applied_flip: Option<&LastAppliedFlip>,
) -> Result<(), DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
{
verify_repair_postcondition_with_topology(
tds,
kernel,
Some(seed_simplices),
GlobalTopology::DEFAULT,
PostconditionMode::Repair,
last_applied_flip,
ConnectivityPostcondition::Defer,
)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum PostconditionMode {
Repair,
Strict,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum ConnectivityPostcondition {
Check,
Defer,
}
pub(super) fn verification_failed(
context: DelaunayRepairVerificationContext,
source: FlipError,
) -> DelaunayRepairError {
DelaunayRepairError::VerificationFailed {
context,
source: Box::new(source),
}
}
pub(super) fn verify_repair_postcondition_with_topology<K, U, V, const D: usize>(
tds: &Tds<U, V, D>,
kernel: &K,
seed_simplices: Option<&[SimplexKey]>,
global_topology: GlobalTopology<D>,
mode: PostconditionMode,
last_applied_flip: Option<&LastAppliedFlip>,
connectivity: ConnectivityPostcondition,
) -> Result<(), DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
{
let topology_model = global_topology.model();
verify_repair_postcondition_locally(
tds,
kernel,
seed_simplices,
&topology_model,
mode,
last_applied_flip,
connectivity,
)
}
pub(super) fn verify_repair_postcondition_locally<K, U, V, const D: usize>(
tds: &Tds<U, V, D>,
kernel: &K,
seed_simplices: Option<&[SimplexKey]>,
topology_model: &GlobalTopologyModelAdapter<D>,
mode: PostconditionMode,
last_applied_flip: Option<&LastAppliedFlip>,
connectivity: ConnectivityPostcondition,
) -> Result<(), DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
{
let config = RepairAttemptConfig {
attempt: 0,
queue_order: RepairQueueOrder::Fifo,
max_flips_override: None,
};
let mut stats = DelaunayRepairStats::default();
let mut diagnostics = RepairDiagnostics::default();
let mut queues = RepairQueues::new();
let _ = seed_repair_queues(tds, seed_simplices, &mut queues, &mut stats)?;
if repair_trace_enabled() {
let seed_count = seed_simplices.map_or(0, <[SimplexKey]>::len);
tracing::debug!(
"[repair] attempt={} order={:?} simplices={} seeds={} queues(facet={}, ridge={}, edge={}, tri={})",
config.attempt,
config.queue_order,
tds.number_of_simplices(),
seed_count,
queues.facet_queue.len(),
queues.ridge_queue.len(),
queues.edge_queue.len(),
queues.triangle_queue.len(),
);
}
verify_postcondition_k2_facets(
tds,
kernel,
topology_model,
&mut queues.facet_queue,
&config,
&mut diagnostics,
mode,
last_applied_flip,
)?;
verify_postcondition_k3_ridges(
tds,
kernel,
topology_model,
&mut queues.ridge_queue,
&config,
&mut diagnostics,
mode,
last_applied_flip,
)?;
verify_postcondition_inverse_k2_edges(
tds,
kernel,
topology_model,
&mut queues.edge_queue,
&config,
&mut diagnostics,
mode,
)?;
verify_postcondition_inverse_k3_triangles(
tds,
kernel,
topology_model,
&mut queues.triangle_queue,
&config,
&mut diagnostics,
mode,
)?;
if connectivity == ConnectivityPostcondition::Check && !tds.is_connected() {
return Err(DelaunayRepairError::PostconditionFailed {
reason: Box::new(DelaunayRepairPostconditionFailure::Disconnected {
simplex_count: tds.number_of_simplices(),
}),
});
}
Ok(())
}
pub(super) fn resolve_postcondition_predicate_failure(
mode: PostconditionMode,
context: DelaunayRepairVerificationContext,
error: &FlipError,
) -> Result<(), DelaunayRepairError> {
match mode {
PostconditionMode::Repair => Ok(()),
PostconditionMode::Strict => Err(verification_failed(context, error.clone())),
}
}
#[expect(
clippy::too_many_arguments,
reason = "Postcondition replay threads topology, diagnostics, and predecessor context explicitly"
)]
pub(super) fn verify_postcondition_k2_facets<K, U, V, const D: usize>(
tds: &Tds<U, V, D>,
kernel: &K,
topology_model: &GlobalTopologyModelAdapter<D>,
queue: &mut VecDeque<(FacetHandle, u64)>,
config: &RepairAttemptConfig,
diagnostics: &mut RepairDiagnostics,
mode: PostconditionMode,
last_applied_flip: Option<&LastAppliedFlip>,
) -> Result<(), DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
{
while let Some((facet, _key)) = pop_queue(queue, config.queue_order) {
let context = match build_k2_flip_context(tds, facet) {
Ok(ctx) => ctx,
Err(
FlipError::BoundaryFacet { .. }
| FlipError::MissingSimplex { .. }
| FlipError::MissingNeighbor { .. }
| FlipError::InvalidFacetAdjacency { .. }
| FlipError::InvalidFacetIndex { .. },
) => {
continue;
}
Err(e) => return Err(e.into()),
};
match is_delaunay_violation_k2(tds, kernel, topology_model, &context, config, diagnostics) {
Ok(true) => {
let flip_degenerate = match k2_flip_would_create_degenerate_simplex(tds, &context) {
Ok(degenerate) => degenerate,
Err(error @ FlipError::PredicateFailure { .. }) => {
resolve_postcondition_predicate_failure(
mode,
DelaunayRepairVerificationContext::LocalK2DegeneracyVerification,
&error,
)?;
continue;
}
Err(e) => {
return Err(verification_failed(
DelaunayRepairVerificationContext::LocalK2DegeneracyVerification,
e,
));
}
};
if flip_degenerate {
if repair_trace_enabled() {
tracing::debug!(
"[repair] postcondition k=2 violation unresolved due to degenerate flip (facet={facet:?})"
);
}
continue;
}
if repair_trace_enabled() {
tracing::debug!(
"[repair] postcondition k=2 violation remains (facet={facet:?})"
);
}
debug_postcondition_facet_context(
tds,
facet,
&context,
diagnostics,
last_applied_flip,
);
let debug_details = if env::var_os("DELAUNAY_REPAIR_DEBUG_FACETS").is_some() {
let removed_details: Vec<_> = context
.removed_face_vertices
.iter()
.filter_map(|&vkey| tds.vertex(vkey).map(|vertex| (vkey, *vertex.point())))
.collect();
let inserted_details: Vec<_> = context
.inserted_face_vertices
.iter()
.filter_map(|&vkey| tds.vertex(vkey).map(|vertex| (vkey, *vertex.point())))
.collect();
Some(format!(
"removed_face={removed_details:?}; inserted_face={inserted_details:?}"
))
} else {
None
};
return Err(DelaunayRepairError::PostconditionFailed {
reason: Box::new(DelaunayRepairPostconditionFailure::LocalK2Violation {
facet,
debug_details,
}),
});
}
Ok(false) => {
}
Err(error @ FlipError::PredicateFailure { .. }) => {
resolve_postcondition_predicate_failure(
mode,
DelaunayRepairVerificationContext::LocalK2PostconditionVerification,
&error,
)?;
}
Err(e) => {
return Err(verification_failed(
DelaunayRepairVerificationContext::LocalK2PostconditionVerification,
e,
));
}
}
}
Ok(())
}
#[expect(
clippy::too_many_arguments,
reason = "Postcondition replay threads topology, diagnostics, and predecessor context explicitly (matches k=2 signature)"
)]
pub(super) fn verify_postcondition_k3_ridges<K, U, V, const D: usize>(
tds: &Tds<U, V, D>,
kernel: &K,
topology_model: &GlobalTopologyModelAdapter<D>,
queue: &mut VecDeque<(RidgeHandle, u64)>,
config: &RepairAttemptConfig,
diagnostics: &mut RepairDiagnostics,
mode: PostconditionMode,
last_applied_flip: Option<&LastAppliedFlip>,
) -> Result<(), DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
{
while let Some((ridge, _key)) = pop_queue(queue, config.queue_order) {
let context = match build_k3_flip_context(tds, ridge) {
Ok(ctx) => ctx,
Err(
FlipError::InvalidRidgeIndex { .. }
| FlipError::InvalidRidgeAdjacency { .. }
| FlipError::InvalidRidgeMultiplicity { .. }
| FlipError::MissingSimplex { .. },
) => {
continue;
}
Err(e) => return Err(e.into()),
};
match is_delaunay_violation_k3(tds, kernel, topology_model, &context, config, diagnostics) {
Ok(true) => {
let flip_degenerate = match flip_would_create_degenerate_simplex(
tds,
&context.removed_face_vertices,
&context.inserted_face_vertices,
) {
Ok(degenerate) => degenerate,
Err(error @ FlipError::PredicateFailure { .. }) => {
resolve_postcondition_predicate_failure(
mode,
DelaunayRepairVerificationContext::LocalK3DegeneracyVerification,
&error,
)?;
continue;
}
Err(e) => {
return Err(verification_failed(
DelaunayRepairVerificationContext::LocalK3DegeneracyVerification,
e,
));
}
};
if flip_degenerate {
if repair_trace_enabled() {
tracing::debug!(
"[repair] postcondition k=3 violation unresolved due to degenerate flip (ridge={ridge:?})"
);
}
continue;
}
if repair_trace_enabled() {
tracing::debug!(
"[repair] postcondition k=3 violation remains (ridge={ridge:?})"
);
}
if repair_ridge_debug_enabled() {
debug_ridge_context(tds, ridge, None, diagnostics, last_applied_flip);
}
return Err(DelaunayRepairError::PostconditionFailed {
reason: Box::new(DelaunayRepairPostconditionFailure::LocalK3Violation {
ridge,
}),
});
}
Ok(false) => {
}
Err(error @ FlipError::PredicateFailure { .. }) => {
resolve_postcondition_predicate_failure(
mode,
DelaunayRepairVerificationContext::LocalK3PostconditionVerification,
&error,
)?;
}
Err(e) => {
return Err(verification_failed(
DelaunayRepairVerificationContext::LocalK3PostconditionVerification,
e,
));
}
}
}
Ok(())
}
pub(super) fn verify_postcondition_inverse_k2_edges<K, U, V, const D: usize>(
tds: &Tds<U, V, D>,
kernel: &K,
topology_model: &GlobalTopologyModelAdapter<D>,
queue: &mut VecDeque<(EdgeKey, u64)>,
config: &RepairAttemptConfig,
diagnostics: &mut RepairDiagnostics,
mode: PostconditionMode,
) -> Result<(), DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
{
while let Some((edge, _key)) = pop_queue(queue, config.queue_order) {
let context = match build_k2_flip_context_from_edge(tds, edge) {
Ok(ctx) => ctx,
Err(
FlipError::InvalidEdgeMultiplicity { .. }
| FlipError::InvalidEdgeAdjacency { .. }
| FlipError::MissingSimplex { .. }
| FlipError::MissingVertex { .. },
) => {
continue;
}
Err(e) => return Err(e.into()),
};
if context.removed_face_vertices.len() != 2 {
continue;
}
let opposite_a = context.removed_face_vertices[0];
let opposite_b = context.removed_face_vertices[1];
let frame_simplex = removed_simplex_frame(&context.removed_simplices)?;
let violates = match delaunay_violation_k2_for_facet(
tds,
kernel,
topology_model,
&context.inserted_face_vertices,
opposite_a,
opposite_b,
&context.removed_simplices,
Some(frame_simplex),
config,
diagnostics,
) {
Ok(violates) => violates,
Err(error @ FlipError::PredicateFailure { .. }) => {
resolve_postcondition_predicate_failure(
mode,
DelaunayRepairVerificationContext::LocalInverseK2PostconditionVerification,
&error,
)?;
continue;
}
Err(e) => {
return Err(verification_failed(
DelaunayRepairVerificationContext::LocalInverseK2PostconditionVerification,
e,
));
}
};
if violates {
continue;
}
let inverse_kind = BistellarFlipKind::from_validated(2, D).inverse();
match validate_bistellar_flip_dynamic(tds, inverse_kind.k(), &context) {
Ok(_) => {
if repair_trace_enabled() {
tracing::debug!(
"[repair] postcondition inverse k=2 flip still applicable (edge={edge:?})"
);
}
return Err(DelaunayRepairError::PostconditionFailed {
reason: Box::new(
DelaunayRepairPostconditionFailure::LocalInverseK2Violation { edge },
),
});
}
Err(error) if repair_flip_is_unavailable(&error) => {
if repair_trace_enabled() {
tracing::debug!(
"[repair] postcondition inverse k=2 predicate preferred an unavailable flip (edge={edge:?}) reason={error}"
);
}
}
Err(error @ FlipError::PredicateFailure { .. }) => {
resolve_postcondition_predicate_failure(
mode,
DelaunayRepairVerificationContext::LocalInverseK2PostconditionVerification,
&error,
)?;
}
Err(error) => {
return Err(verification_failed(
DelaunayRepairVerificationContext::LocalInverseK2PostconditionVerification,
error,
));
}
}
}
Ok(())
}
pub(super) fn verify_postcondition_inverse_k3_triangles<K, U, V, const D: usize>(
tds: &Tds<U, V, D>,
kernel: &K,
topology_model: &GlobalTopologyModelAdapter<D>,
queue: &mut VecDeque<(TriangleHandle, u64)>,
config: &RepairAttemptConfig,
diagnostics: &mut RepairDiagnostics,
mode: PostconditionMode,
) -> Result<(), DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
{
while let Some((triangle, _key)) = pop_queue(queue, config.queue_order) {
let context = match build_k3_flip_context_from_triangle(tds, triangle) {
Ok(ctx) => ctx,
Err(
FlipError::InvalidTriangleMultiplicity { .. }
| FlipError::InvalidTriangleAdjacency { .. }
| FlipError::MissingSimplex { .. }
| FlipError::MissingVertex { .. },
) => {
continue;
}
Err(e) => return Err(e.into()),
};
let frame_simplex = removed_simplex_frame(&context.removed_simplices)?;
let violates = match delaunay_violation_k3_for_ridge(
tds,
kernel,
topology_model,
&context.inserted_face_vertices,
&context.removed_face_vertices,
&context.removed_simplices,
Some(frame_simplex),
config,
diagnostics,
) {
Ok(violates) => violates,
Err(error @ FlipError::PredicateFailure { .. }) => {
resolve_postcondition_predicate_failure(
mode,
DelaunayRepairVerificationContext::LocalInverseK3PostconditionVerification,
&error,
)?;
continue;
}
Err(e) => {
return Err(verification_failed(
DelaunayRepairVerificationContext::LocalInverseK3PostconditionVerification,
e,
));
}
};
if violates {
continue;
}
let inverse_kind = BistellarFlipKind::from_validated(3, D).inverse();
match validate_bistellar_flip_dynamic(tds, inverse_kind.k(), &context) {
Ok(_) => {
if repair_trace_enabled() {
tracing::debug!(
"[repair] postcondition inverse k=3 flip still applicable (triangle={triangle:?})"
);
}
return Err(DelaunayRepairError::PostconditionFailed {
reason: Box::new(
DelaunayRepairPostconditionFailure::LocalInverseK3Violation { triangle },
),
});
}
Err(error) if repair_flip_is_unavailable(&error) => {
if repair_trace_enabled() {
tracing::debug!(
"[repair] postcondition inverse k=3 predicate preferred an unavailable flip (triangle={triangle:?}) reason={error}"
);
}
}
Err(error @ FlipError::PredicateFailure { .. }) => {
resolve_postcondition_predicate_failure(
mode,
DelaunayRepairVerificationContext::LocalInverseK3PostconditionVerification,
&error,
)?;
}
Err(error) => {
return Err(verification_failed(
DelaunayRepairVerificationContext::LocalInverseK3PostconditionVerification,
error,
));
}
}
}
Ok(())
}
pub(super) const AMBIGUOUS_SAMPLE_LIMIT: usize = 16;
pub(super) const CYCLE_SAMPLE_LIMIT: usize = 16;
pub(super) const FLIP_SIGNATURE_WINDOW: usize = 4096;
#[cfg(test)]
mod tests {
use super::super::test_support::init_tracing;
use super::super::*;
use super::*;
use crate::core::algorithms::insertion::repair_neighbor_pointers;
use crate::core::tds::TdsBuilder;
use crate::core::test_support::snapshot_topology;
use crate::geometry::kernel::{AdaptiveKernel, FastKernel};
use crate::triangulation::validation::TopologyGuarantee;
use crate::vertex;
use slotmap::KeyData;
use std::assert_matches;
use std::iter::once;
fn verify_tds_via_flip_predicates<K, U, V, const D: usize>(
tds: &Tds<U, V, D>,
kernel: &K,
) -> Result<(), DelaunayRepairError>
where
K: Kernel<D, Scalar = f64>,
U: DataType,
V: DataType,
{
verify_delaunay_with_topology(tds, kernel, GlobalTopology::DEFAULT)
}
fn two_triangle_tds() -> Tds<(), (), 2> {
let vertices = [
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.0, 1.0]).unwrap(),
vertex!([1.0, 0.2]).unwrap(),
];
let simplices = [vec![0, 1, 3], vec![0, 3, 2]];
TdsBuilder::new(&vertices, &simplices).build().unwrap()
}
fn synthetic_simplex_key(index: u64) -> SimplexKey {
SimplexKey::from(KeyData::from_ffi(index))
}
#[test]
fn test_local_postcondition_frontier_deduplicates_seed_and_touched_simplices() {
let seed_a = synthetic_simplex_key(1);
let seed_b = synthetic_simplex_key(2);
let touched_a = synthetic_simplex_key(3);
let frontier = local_postcondition_frontier(
&[seed_a, seed_b, seed_a],
&[seed_b, touched_a, touched_a],
);
assert_eq!(frontier.len(), 3);
assert_eq!(frontier[0], seed_a);
assert_eq!(frontier[1], seed_b);
assert_eq!(frontier[2], touched_a);
}
#[test]
fn test_repair_postcondition_required_tracks_mutation_or_applicable_site() {
let mut stats = DelaunayRepairStats::default();
let mut diagnostics = RepairDiagnostics::default();
assert!(!repair_postcondition_required(&stats, &diagnostics));
diagnostics.record_applicable_repair_site();
assert!(repair_postcondition_required(&stats, &diagnostics));
diagnostics = RepairDiagnostics::default();
stats.flips_performed = 1;
assert!(repair_postcondition_required(&stats, &diagnostics));
}
#[test]
fn test_repair_delaunay_flips_non_delaunay_edge_2d() {
init_tracing();
let kernel = AdaptiveKernel::<f64>::new();
let a_coords = [0.0, 0.0];
let b_coords = [1.0, 1.0];
let c_coords = [1.0, 0.0];
let d_candidates = [[0.0, 1.2], [0.1, 1.1], [0.2, 0.9], [-0.1, 1.3]];
let mut tds = None;
for d_coords in d_candidates {
let mut candidate: Tds<(), (), 2> = Tds::empty();
let a = candidate
.insert_vertex_with_mapping(vertex!(a_coords).unwrap())
.unwrap();
let b = candidate
.insert_vertex_with_mapping(vertex!(b_coords).unwrap())
.unwrap();
let c = candidate
.insert_vertex_with_mapping(vertex!(c_coords).unwrap())
.unwrap();
let d = candidate
.insert_vertex_with_mapping(vertex!(d_coords).unwrap())
.unwrap();
let _c1 = candidate
.insert_simplex_with_mapping(
Simplex::try_new_with_data(vec![a, b, c], None).unwrap(),
)
.unwrap();
let _c2 = candidate
.insert_simplex_with_mapping(
Simplex::try_new_with_data(vec![a, b, d], None).unwrap(),
)
.unwrap();
repair_neighbor_pointers(&mut candidate).unwrap();
if verify_tds_via_flip_predicates(&candidate, &kernel).is_err() {
tds = Some(candidate);
break;
}
}
let mut tds = tds.expect("expected a non-Delaunay configuration from candidates");
let stats = repair_delaunay_with_flips_k2_k3(
&mut tds,
&kernel,
None,
TopologyGuarantee::PLManifold,
GlobalTopology::DEFAULT,
None,
)
.unwrap();
assert!(stats.flips_performed > 0);
assert!(verify_tds_via_flip_predicates(&tds, &kernel).is_ok());
assert!(tds.is_valid().is_ok());
}
#[test]
fn test_repair_max_flips_override_caps_repair() {
init_tracing();
let kernel = AdaptiveKernel::<f64>::new();
let d_candidates = [[0.0, 1.2], [0.1, 1.1], [0.2, 0.9], [-0.1, 1.3]];
let mut tds = None;
for d_coords in d_candidates {
let mut candidate: Tds<(), (), 2> = Tds::empty();
let a = candidate
.insert_vertex_with_mapping(vertex!([0.0, 0.0]).unwrap())
.unwrap();
let b = candidate
.insert_vertex_with_mapping(vertex!([1.0, 1.0]).unwrap())
.unwrap();
let c = candidate
.insert_vertex_with_mapping(vertex!([1.0, 0.0]).unwrap())
.unwrap();
let d = candidate
.insert_vertex_with_mapping(vertex!(d_coords).unwrap())
.unwrap();
let _c1 = candidate
.insert_simplex_with_mapping(
Simplex::try_new_with_data(vec![a, b, c], None).unwrap(),
)
.unwrap();
let _c2 = candidate
.insert_simplex_with_mapping(
Simplex::try_new_with_data(vec![a, b, d], None).unwrap(),
)
.unwrap();
repair_neighbor_pointers(&mut candidate).unwrap();
if verify_tds_via_flip_predicates(&candidate, &kernel).is_err() {
tds = Some(candidate);
break;
}
}
let mut tds = tds.expect("expected a non-Delaunay configuration from candidates");
let before = snapshot_topology(&tds);
let result = repair_delaunay_with_flips_k2_k3(
&mut tds,
&kernel,
None,
TopologyGuarantee::PLManifold,
GlobalTopology::DEFAULT,
Some(0),
);
match result {
Err(DelaunayRepairError::NonConvergent { diagnostics, .. }) => {
assert_eq!(
diagnostics.flips_performed, 0,
"max_flips_override=Some(0) should prevent any flips, got: {}",
diagnostics.flips_performed
);
}
other => panic!("expected NonConvergent, got: {other:?}"),
}
assert_eq!(
snapshot_topology(&tds),
before,
"TDS must remain unchanged when max_flips=0 prevents all flips"
);
}
#[test]
#[expect(
clippy::many_single_char_names,
reason = "vertex names a-e mirror standard simplex labelling in geometry tests"
)]
fn test_repair_max_flips_override_caps_repair_3d() {
init_tracing();
let kernel = AdaptiveKernel::<f64>::new();
let mut tds: Tds<(), (), 3> = Tds::empty();
let a = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]).unwrap())
.unwrap();
let b = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]).unwrap())
.unwrap();
let c = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]).unwrap())
.unwrap();
let d = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0]).unwrap())
.unwrap();
let e = tds
.insert_vertex_with_mapping(vertex!([0.3, 0.3, 0.3]).unwrap())
.unwrap();
let _c1 = tds
.insert_simplex_with_mapping(
Simplex::try_new_with_data(vec![a, b, c, d], None).unwrap(),
)
.unwrap();
let _c2 = tds
.insert_simplex_with_mapping(
Simplex::try_new_with_data(vec![a, b, c, e], None).unwrap(),
)
.unwrap();
repair_neighbor_pointers(&mut tds).unwrap();
assert!(
verify_tds_via_flip_predicates(&tds, &kernel).is_err(),
"3D fixture must be non-Delaunay (e inside circumsphere of {{a,b,c,d}})"
);
let before = snapshot_topology(&tds);
let result = repair_delaunay_with_flips_k2_k3(
&mut tds,
&kernel,
None,
TopologyGuarantee::PLManifold,
GlobalTopology::DEFAULT,
Some(0),
);
match result {
Err(DelaunayRepairError::NonConvergent { diagnostics, .. }) => {
assert_eq!(diagnostics.flips_performed, 0);
}
other => panic!("expected NonConvergent for 3D, got: {other:?}"),
}
assert_eq!(
snapshot_topology(&tds),
before,
"3D TDS must remain unchanged when max_flips=0 prevents all flips"
);
}
#[test]
fn test_verify_tds_via_flip_predicates_reports_non_delaunay_2d() {
init_tracing();
let kernel = FastKernel::<f64>::new();
let a_coords = [0.0, 0.0];
let b_coords = [1.0, 1.0];
let c_coords = [1.0, 0.0];
let d_candidates = [[0.0, 1.2], [0.1, 1.1], [0.2, 0.9], [-0.1, 1.3]];
let mut tds = None;
for d_coords in d_candidates {
let mut candidate: Tds<(), (), 2> = Tds::empty();
let a = candidate
.insert_vertex_with_mapping(vertex!(a_coords).unwrap())
.unwrap();
let b = candidate
.insert_vertex_with_mapping(vertex!(b_coords).unwrap())
.unwrap();
let c = candidate
.insert_vertex_with_mapping(vertex!(c_coords).unwrap())
.unwrap();
let d = candidate
.insert_vertex_with_mapping(vertex!(d_coords).unwrap())
.unwrap();
let _c1 = candidate
.insert_simplex_with_mapping(
Simplex::try_new_with_data(vec![a, b, c], None).unwrap(),
)
.unwrap();
let _c2 = candidate
.insert_simplex_with_mapping(
Simplex::try_new_with_data(vec![a, b, d], None).unwrap(),
)
.unwrap();
repair_neighbor_pointers(&mut candidate).unwrap();
if verify_tds_via_flip_predicates(&candidate, &kernel).is_err() {
tds = Some(candidate);
break;
}
}
let tds = tds.expect("expected a non-Delaunay configuration from candidates");
let result = verify_tds_via_flip_predicates(&tds, &kernel);
assert_matches!(result, Err(DelaunayRepairError::PostconditionFailed { .. }));
}
#[test]
fn test_repair_delaunay_with_flips_rejects_unsupported_dimension_1d() {
init_tracing();
let mut tds: Tds<(), (), 1> = Tds::empty();
let kernel = AdaptiveKernel::<f64>::new();
let result = repair_delaunay_with_flips_k2_k3(
&mut tds,
&kernel,
None,
TopologyGuarantee::PLManifold,
GlobalTopology::DEFAULT,
None,
);
assert_matches!(
result,
Err(DelaunayRepairError::Flip { source })
if matches!(
source.as_ref(),
FlipError::UnsupportedDimension { dimension: 1 }
)
);
}
#[test]
fn test_repair_run_full_reseed_preserves_mutation_frontier() {
init_tracing();
let tds = two_triangle_tds();
let local_simplex = tds.simplex_keys().next().unwrap();
let outcome = RepairAttemptOutcome {
postcondition_required: false,
stats: DelaunayRepairStats::default(),
last_applied_flip: None,
touched_simplices: once(local_simplex).collect(),
used_full_reseed: true,
};
let run = repair_run_from_attempt(outcome);
assert!(run.used_full_reseed);
assert!(
tds.simplex_keys().count() > 1,
"fixture should distinguish local and full frontiers"
);
assert_eq!(run.touched_simplices.len(), 1);
assert_eq!(run.touched_simplices[0], local_simplex);
}
#[test]
fn test_repair_k2_empty_seed_does_not_full_reseed() {
init_tracing();
let mut tds = two_triangle_tds();
let before = snapshot_topology(&tds);
let kernel = AdaptiveKernel::<f64>::new();
let config = RepairAttemptConfig {
attempt: 1,
queue_order: RepairQueueOrder::Fifo,
max_flips_override: None,
};
let empty_seeds: &[SimplexKey] = &[];
let topology_model = GlobalTopology::DEFAULT.model();
let outcome = repair_delaunay_with_flips_k2_attempt(
&mut tds,
&kernel,
Some(empty_seeds),
&topology_model,
&config,
)
.unwrap();
assert!(!outcome.used_full_reseed);
assert_eq!(outcome.stats.facets_checked, 0);
assert!(outcome.touched_simplices.is_empty());
assert_eq!(snapshot_topology(&tds), before);
}
#[test]
fn test_repair_queue_k2_local_seed() {
init_tracing();
let mut tds = two_triangle_tds();
let kernel = AdaptiveKernel::<f64>::new();
let seed_simplex = tds.simplex_keys().next().unwrap();
let stats = repair_delaunay_with_flips_k2_k3(
&mut tds,
&kernel,
Some(&[seed_simplex]),
TopologyGuarantee::PLManifold,
GlobalTopology::DEFAULT,
None,
)
.unwrap();
assert!(stats.facets_checked > 0);
assert!(tds.is_valid().is_ok());
}
}