#![forbid(unsafe_code)]
use crate::config::CdtTopology;
use crate::errors::{BackendMutationOperation, CdtError};
use crate::geometry::CdtTriangulation2D;
use crate::geometry::backends::delaunay::{
DelaunayEdgeHandle, DelaunayFaceHandle, DelaunayVertexHandle,
};
use crate::geometry::traits::{EdgeAdjacentFaces, TriangulationMut, TriangulationQuery};
use rand::{RngExt, SeedableRng, rngs::Xoshiro256PlusPlus};
use serde::{Deserialize, Deserializer, Serialize};
use std::array;
use std::fmt::Display;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MoveType {
Move22,
Move13Add,
Move31Remove,
EdgeFlip,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MoveResult {
Success,
CausalityViolation,
GeometricViolation,
Rejected(CdtError),
HardFailure(CdtError),
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct MoveStatistics {
moves_22_attempted: u64,
moves_22_accepted: u64,
#[serde(default)]
moves_22_hard_failed: u64,
moves_13_attempted: u64,
moves_13_accepted: u64,
#[serde(default)]
moves_13_hard_failed: u64,
moves_31_attempted: u64,
moves_31_accepted: u64,
#[serde(default)]
moves_31_hard_failed: u64,
edge_flips_attempted: u64,
edge_flips_accepted: u64,
#[serde(default)]
edge_flips_hard_failed: u64,
}
#[derive(Deserialize)]
struct MoveStatisticsWire {
moves_22_attempted: u64,
moves_22_accepted: u64,
#[serde(default)]
moves_22_hard_failed: u64,
moves_13_attempted: u64,
moves_13_accepted: u64,
#[serde(default)]
moves_13_hard_failed: u64,
moves_31_attempted: u64,
moves_31_accepted: u64,
#[serde(default)]
moves_31_hard_failed: u64,
edge_flips_attempted: u64,
edge_flips_accepted: u64,
#[serde(default)]
edge_flips_hard_failed: u64,
}
impl<'de> Deserialize<'de> for MoveStatistics {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let wire = MoveStatisticsWire::deserialize(deserializer)?;
Self::from_wire(&wire).map_err(serde::de::Error::custom)
}
}
impl MoveStatistics {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[cfg(test)]
#[expect(
clippy::too_many_arguments,
reason = "test and serde helpers need to preserve the flat move-statistics wire shape"
)]
pub(crate) const fn from_validated_parts(
moves_22_attempted: u64,
moves_22_accepted: u64,
moves_22_hard_failed: u64,
moves_13_attempted: u64,
moves_13_accepted: u64,
moves_13_hard_failed: u64,
moves_31_attempted: u64,
moves_31_accepted: u64,
moves_31_hard_failed: u64,
edge_flips_attempted: u64,
edge_flips_accepted: u64,
edge_flips_hard_failed: u64,
) -> Self {
Self {
moves_22_attempted,
moves_22_accepted,
moves_22_hard_failed,
moves_13_attempted,
moves_13_accepted,
moves_13_hard_failed,
moves_31_attempted,
moves_31_accepted,
moves_31_hard_failed,
edge_flips_attempted,
edge_flips_accepted,
edge_flips_hard_failed,
}
}
fn from_wire(wire: &MoveStatisticsWire) -> Result<Self, String> {
validate_move_counter(
MoveType::Move22,
wire.moves_22_attempted,
wire.moves_22_accepted,
wire.moves_22_hard_failed,
)?;
validate_move_counter(
MoveType::Move13Add,
wire.moves_13_attempted,
wire.moves_13_accepted,
wire.moves_13_hard_failed,
)?;
validate_move_counter(
MoveType::Move31Remove,
wire.moves_31_attempted,
wire.moves_31_accepted,
wire.moves_31_hard_failed,
)?;
validate_move_counter(
MoveType::EdgeFlip,
wire.edge_flips_attempted,
wire.edge_flips_accepted,
wire.edge_flips_hard_failed,
)?;
Ok(Self {
moves_22_attempted: wire.moves_22_attempted,
moves_22_accepted: wire.moves_22_accepted,
moves_22_hard_failed: wire.moves_22_hard_failed,
moves_13_attempted: wire.moves_13_attempted,
moves_13_accepted: wire.moves_13_accepted,
moves_13_hard_failed: wire.moves_13_hard_failed,
moves_31_attempted: wire.moves_31_attempted,
moves_31_accepted: wire.moves_31_accepted,
moves_31_hard_failed: wire.moves_31_hard_failed,
edge_flips_attempted: wire.edge_flips_attempted,
edge_flips_accepted: wire.edge_flips_accepted,
edge_flips_hard_failed: wire.edge_flips_hard_failed,
})
}
pub const fn record_attempt(&mut self, move_type: MoveType) {
match move_type {
MoveType::Move22 => {
self.moves_22_attempted = self.moves_22_attempted.saturating_add(1);
}
MoveType::Move13Add => {
self.moves_13_attempted = self.moves_13_attempted.saturating_add(1);
}
MoveType::Move31Remove => {
self.moves_31_attempted = self.moves_31_attempted.saturating_add(1);
}
MoveType::EdgeFlip => {
self.edge_flips_attempted = self.edge_flips_attempted.saturating_add(1);
}
}
}
pub const fn record_success(&mut self, move_type: MoveType) {
match move_type {
MoveType::Move22 => {
if self
.moves_22_accepted
.saturating_add(self.moves_22_hard_failed)
>= self.moves_22_attempted
{
self.moves_22_attempted = self.moves_22_attempted.saturating_add(1);
}
self.moves_22_accepted = self.moves_22_accepted.saturating_add(1);
}
MoveType::Move13Add => {
if self
.moves_13_accepted
.saturating_add(self.moves_13_hard_failed)
>= self.moves_13_attempted
{
self.moves_13_attempted = self.moves_13_attempted.saturating_add(1);
}
self.moves_13_accepted = self.moves_13_accepted.saturating_add(1);
}
MoveType::Move31Remove => {
if self
.moves_31_accepted
.saturating_add(self.moves_31_hard_failed)
>= self.moves_31_attempted
{
self.moves_31_attempted = self.moves_31_attempted.saturating_add(1);
}
self.moves_31_accepted = self.moves_31_accepted.saturating_add(1);
}
MoveType::EdgeFlip => {
if self
.edge_flips_accepted
.saturating_add(self.edge_flips_hard_failed)
>= self.edge_flips_attempted
{
self.edge_flips_attempted = self.edge_flips_attempted.saturating_add(1);
}
self.edge_flips_accepted = self.edge_flips_accepted.saturating_add(1);
}
}
}
pub const fn record_hard_failure(&mut self, move_type: MoveType) {
match move_type {
MoveType::Move22 => {
if self
.moves_22_accepted
.saturating_add(self.moves_22_hard_failed)
>= self.moves_22_attempted
{
self.moves_22_attempted = self.moves_22_attempted.saturating_add(1);
}
self.moves_22_hard_failed = self.moves_22_hard_failed.saturating_add(1);
}
MoveType::Move13Add => {
if self
.moves_13_accepted
.saturating_add(self.moves_13_hard_failed)
>= self.moves_13_attempted
{
self.moves_13_attempted = self.moves_13_attempted.saturating_add(1);
}
self.moves_13_hard_failed = self.moves_13_hard_failed.saturating_add(1);
}
MoveType::Move31Remove => {
if self
.moves_31_accepted
.saturating_add(self.moves_31_hard_failed)
>= self.moves_31_attempted
{
self.moves_31_attempted = self.moves_31_attempted.saturating_add(1);
}
self.moves_31_hard_failed = self.moves_31_hard_failed.saturating_add(1);
}
MoveType::EdgeFlip => {
if self
.edge_flips_accepted
.saturating_add(self.edge_flips_hard_failed)
>= self.edge_flips_attempted
{
self.edge_flips_attempted = self.edge_flips_attempted.saturating_add(1);
}
self.edge_flips_hard_failed = self.edge_flips_hard_failed.saturating_add(1);
}
}
}
#[must_use]
pub fn acceptance_rate(&self, move_type: MoveType) -> f64 {
let (attempted, accepted) = match move_type {
MoveType::Move22 => (self.moves_22_attempted, self.moves_22_accepted),
MoveType::Move13Add => (self.moves_13_attempted, self.moves_13_accepted),
MoveType::Move31Remove => (self.moves_31_attempted, self.moves_31_accepted),
MoveType::EdgeFlip => (self.edge_flips_attempted, self.edge_flips_accepted),
};
if attempted == 0 {
0.0
} else {
let accepted = count_to_f64(accepted);
let attempted = count_to_f64(attempted);
accepted / attempted
}
}
#[must_use]
pub fn total_acceptance_rate(&self) -> f64 {
let total_attempted = self.total_attempted();
let total_accepted = self.total_accepted();
if total_attempted == 0 {
0.0
} else {
let total_accepted = count_to_f64(total_accepted);
let total_attempted = count_to_f64(total_attempted);
total_accepted / total_attempted
}
}
#[must_use]
pub const fn total_attempted(&self) -> u64 {
self.moves_22_attempted
.saturating_add(self.moves_13_attempted)
.saturating_add(self.moves_31_attempted)
.saturating_add(self.edge_flips_attempted)
}
#[must_use]
pub const fn total_accepted(&self) -> u64 {
self.moves_22_accepted
.saturating_add(self.moves_13_accepted)
.saturating_add(self.moves_31_accepted)
.saturating_add(self.edge_flips_accepted)
}
#[must_use]
pub const fn total_hard_failures(&self) -> u64 {
self.moves_22_hard_failed
.saturating_add(self.moves_13_hard_failed)
.saturating_add(self.moves_31_hard_failed)
.saturating_add(self.edge_flips_hard_failed)
}
#[must_use]
pub const fn attempted(&self, move_type: MoveType) -> u64 {
match move_type {
MoveType::Move22 => self.moves_22_attempted,
MoveType::Move13Add => self.moves_13_attempted,
MoveType::Move31Remove => self.moves_31_attempted,
MoveType::EdgeFlip => self.edge_flips_attempted,
}
}
#[must_use]
pub const fn accepted(&self, move_type: MoveType) -> u64 {
match move_type {
MoveType::Move22 => self.moves_22_accepted,
MoveType::Move13Add => self.moves_13_accepted,
MoveType::Move31Remove => self.moves_31_accepted,
MoveType::EdgeFlip => self.edge_flips_accepted,
}
}
#[must_use]
pub const fn hard_failed(&self, move_type: MoveType) -> u64 {
match move_type {
MoveType::Move22 => self.moves_22_hard_failed,
MoveType::Move13Add => self.moves_13_hard_failed,
MoveType::Move31Remove => self.moves_31_hard_failed,
MoveType::EdgeFlip => self.edge_flips_hard_failed,
}
}
}
fn validate_move_counter(
move_type: MoveType,
attempted: u64,
accepted: u64,
hard_failed: u64,
) -> Result<(), String> {
let terminal = accepted.checked_add(hard_failed).ok_or_else(|| {
format!("{move_type:?} accepted plus hard-failure counters exceed u64::MAX")
})?;
if terminal > attempted {
Err(format!(
"{move_type:?} accepted plus hard-failure counters ({terminal}) exceed attempted counter ({attempted})"
))
} else {
Ok(())
}
}
#[expect(
clippy::cast_precision_loss,
reason = "move counters are converted only for aggregate acceptance-rate reporting"
)]
const fn count_to_f64(count: u64) -> f64 {
count as f64
}
#[derive(Clone, Serialize, Deserialize)]
pub struct ErgodicsSystem {
stats: MoveStatistics,
rng: Xoshiro256PlusPlus,
#[serde(skip)]
site_cache: MoveSiteCache,
}
#[derive(Clone, Default)]
struct MoveSiteCache {
move_22: MoveFamilySites,
move_13_add: MoveFamilySites,
move_31_remove: MoveFamilySites,
edge_flip: MoveFamilySites,
}
#[derive(Clone, Default)]
struct MoveFamilySites {
instance_id: Option<u64>,
modification_count: Option<u64>,
sites: Vec<ProposalSite>,
geometric_candidate_seen: bool,
}
impl MoveSiteCache {
fn ensure_current(&mut self, triangulation: &CdtTriangulation2D, move_type: MoveType) {
let instance_id = triangulation.instance_id();
let modification_count = triangulation.metadata().modification_count();
let family = self.family(move_type);
if family.instance_id == Some(instance_id)
&& family.modification_count == Some(modification_count)
{
return;
}
*self.family_mut(move_type) =
Self::collect_family(triangulation, move_type, instance_id, modification_count);
}
const fn family(&self, move_type: MoveType) -> &MoveFamilySites {
match move_type {
MoveType::Move22 => &self.move_22,
MoveType::Move13Add => &self.move_13_add,
MoveType::Move31Remove => &self.move_31_remove,
MoveType::EdgeFlip => &self.edge_flip,
}
}
const fn family_mut(&mut self, move_type: MoveType) -> &mut MoveFamilySites {
match move_type {
MoveType::Move22 => &mut self.move_22,
MoveType::Move13Add => &mut self.move_13_add,
MoveType::Move31Remove => &mut self.move_31_remove,
MoveType::EdgeFlip => &mut self.edge_flip,
}
}
fn site_count(&mut self, triangulation: &CdtTriangulation2D, move_type: MoveType) -> usize {
self.ensure_current(triangulation, move_type);
self.family(move_type).sites.len()
}
fn collect_family(
triangulation: &CdtTriangulation2D,
move_type: MoveType,
instance_id: u64,
modification_count: u64,
) -> MoveFamilySites {
let mut sites = Vec::new();
let geometric_candidate_seen =
visit_proposal_sites(triangulation, move_type, |site| sites.push(site));
MoveFamilySites {
instance_id: Some(instance_id),
modification_count: Some(modification_count),
sites,
geometric_candidate_seen,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum InsertionLabel {
Unfoliated,
Label(u32),
}
#[derive(Clone)]
pub(crate) struct ToroidalInsertionCandidate {
edge: DelaunayEdgeHandle,
face: DelaunayFaceHandle,
point: [f64; 2],
label: u32,
}
#[derive(Clone)]
pub(crate) struct ToroidalRemovalCandidate {
vertex: DelaunayVertexHandle,
flip_edge: DelaunayEdgeHandle,
}
#[derive(Clone)]
pub(crate) enum ProposalSite {
EdgeFlip(DelaunayEdgeHandle),
FaceSubdivision {
face: DelaunayFaceHandle,
point: [f64; 2],
label: InsertionLabel,
},
ToroidalInsertion(ToroidalInsertionCandidate),
VertexRemoval(DelaunayVertexHandle),
ToroidalRemoval(ToroidalRemovalCandidate),
}
pub(crate) struct ProposalSiteSelection {
pub(crate) site_count: usize,
pub(crate) site: Option<ProposalSite>,
geometric_candidate_seen: bool,
}
impl ErgodicsSystem {
#[must_use]
pub fn new() -> Self {
Self {
stats: MoveStatistics::new(),
rng: rand::make_rng(),
site_cache: MoveSiteCache::default(),
}
}
#[must_use]
pub fn with_seed(seed: u64) -> Self {
Self {
stats: MoveStatistics::new(),
rng: Xoshiro256PlusPlus::seed_from_u64(seed),
site_cache: MoveSiteCache::default(),
}
}
#[must_use]
pub const fn stats(&self) -> &MoveStatistics {
&self.stats
}
pub(crate) const fn replace_stats(&mut self, stats: MoveStatistics) -> MoveStatistics {
std::mem::replace(&mut self.stats, stats)
}
#[must_use]
pub fn select_random_move(&mut self) -> MoveType {
match self.rng.random_range(0..4) {
0 => MoveType::Move22,
1 => MoveType::Move13Add,
2 => MoveType::Move31Remove,
_ => MoveType::EdgeFlip,
}
}
pub(crate) fn select_proposal_site(
&mut self,
triangulation: &CdtTriangulation2D,
move_type: MoveType,
) -> ProposalSiteSelection {
self.site_cache.ensure_current(triangulation, move_type);
let family = self.site_cache.family(move_type);
let site_count = family.sites.len();
let selected_site = if site_count == 0 {
None
} else {
let index = self.rng.random_range(0..site_count);
Some(family.sites[index].clone())
};
ProposalSiteSelection {
site_count,
site: selected_site,
geometric_candidate_seen: family.geometric_candidate_seen,
}
}
pub(crate) fn apply_proposal_site(
&mut self,
triangulation: &mut CdtTriangulation2D,
move_type: MoveType,
site: ProposalSite,
) -> MoveResult {
match (move_type, site) {
(MoveType::Move22 | MoveType::EdgeFlip, ProposalSite::EdgeFlip(edge)) => {
self.apply_edge_flip_site(triangulation, move_type, edge)
}
(MoveType::Move13Add, ProposalSite::FaceSubdivision { face, point, label }) => {
self.apply_face_subdivision_site(triangulation, face, point, label)
}
(MoveType::Move13Add, ProposalSite::ToroidalInsertion(candidate)) => {
self.apply_toroidal_insertion_site(triangulation, candidate)
}
(MoveType::Move31Remove, ProposalSite::VertexRemoval(vertex)) => {
self.apply_vertex_removal_site(triangulation, vertex)
}
(MoveType::Move31Remove, ProposalSite::ToroidalRemoval(candidate)) => {
self.apply_toroidal_removal_site(triangulation, candidate)
}
(
MoveType::Move22 | MoveType::EdgeFlip,
ProposalSite::FaceSubdivision { .. }
| ProposalSite::ToroidalInsertion(_)
| ProposalSite::VertexRemoval(_)
| ProposalSite::ToroidalRemoval(_),
)
| (
MoveType::Move13Add,
ProposalSite::EdgeFlip(_)
| ProposalSite::VertexRemoval(_)
| ProposalSite::ToroidalRemoval(_),
)
| (
MoveType::Move31Remove,
ProposalSite::EdgeFlip(_)
| ProposalSite::FaceSubdivision { .. }
| ProposalSite::ToroidalInsertion(_),
) => MoveResult::GeometricViolation,
}
}
pub fn attempt_22_move(&mut self, triangulation: &mut CdtTriangulation2D) -> MoveResult {
self.stats.record_attempt(MoveType::Move22);
let result = self.attempt_causal_edge_flip(triangulation, MoveType::Move22);
self.record_hard_failure_if_needed(MoveType::Move22, result)
}
pub fn attempt_13_move(&mut self, triangulation: &mut CdtTriangulation2D) -> MoveResult {
self.stats.record_attempt(MoveType::Move13Add);
let result = self.attempt_13_move_mutating(triangulation);
self.record_hard_failure_if_needed(MoveType::Move13Add, result)
}
fn attempt_13_move_mutating(&mut self, triangulation: &mut CdtTriangulation2D) -> MoveResult {
let selection = self.select_proposal_site(triangulation, MoveType::Move13Add);
let Some(site) = selection.site else {
return rejected_empty_selection(triangulation, &selection);
};
self.apply_proposal_site(triangulation, MoveType::Move13Add, site)
}
pub fn attempt_31_move(&mut self, triangulation: &mut CdtTriangulation2D) -> MoveResult {
self.stats.record_attempt(MoveType::Move31Remove);
let result = self.attempt_31_move_mutating(triangulation);
self.record_hard_failure_if_needed(MoveType::Move31Remove, result)
}
fn attempt_31_move_mutating(&mut self, triangulation: &mut CdtTriangulation2D) -> MoveResult {
let selection = self.select_proposal_site(triangulation, MoveType::Move31Remove);
let Some(site) = selection.site else {
return rejected_empty_selection(triangulation, &selection);
};
self.apply_proposal_site(triangulation, MoveType::Move31Remove, site)
}
pub fn attempt_edge_flip(&mut self, triangulation: &mut CdtTriangulation2D) -> MoveResult {
self.stats.record_attempt(MoveType::EdgeFlip);
let result = self.attempt_causal_edge_flip(triangulation, MoveType::EdgeFlip);
self.record_hard_failure_if_needed(MoveType::EdgeFlip, result)
}
pub fn attempt_random_move(&mut self, triangulation: &mut CdtTriangulation2D) -> MoveResult {
let move_type = self.select_random_move();
match move_type {
MoveType::Move22 => self.attempt_22_move(triangulation),
MoveType::Move13Add => self.attempt_13_move(triangulation),
MoveType::Move31Remove => self.attempt_31_move(triangulation),
MoveType::EdgeFlip => self.attempt_edge_flip(triangulation),
}
}
fn attempt_causal_edge_flip(
&mut self,
triangulation: &mut CdtTriangulation2D,
move_type: MoveType,
) -> MoveResult {
let selection = self.select_proposal_site(triangulation, move_type);
let Some(site) = selection.site else {
return rejected_empty_selection(triangulation, &selection);
};
self.apply_proposal_site(triangulation, move_type, site)
}
fn apply_edge_flip_site(
&mut self,
triangulation: &mut CdtTriangulation2D,
move_type: MoveType,
edge: DelaunayEdgeHandle,
) -> MoveResult {
let flip_target = format!("{edge:?}");
let snapshot = triangulation.clone();
let flip_result = triangulation.flip_edge(edge);
let result = match flip_result {
Ok(_) => self.finish_mutated_move(triangulation, move_type),
Err(err) => reject_backend(BackendMutationOperation::FlipEdge, flip_target, &err),
};
rollback_if_failed(triangulation, snapshot, result)
}
fn apply_face_subdivision_site(
&mut self,
triangulation: &mut CdtTriangulation2D,
face: DelaunayFaceHandle,
point: [f64; 2],
label: InsertionLabel,
) -> MoveResult {
let subdivision_target = format!("face {:?}", face.simplex_key());
let snapshot = triangulation.clone();
let subdivision = triangulation.subdivide_face(face, &point);
let subdivision = match subdivision {
Ok(subdivision) => subdivision,
Err(err) => {
let result = reject_backend(
BackendMutationOperation::SubdivideFace,
subdivision_target,
&err,
);
return rollback_if_failed(triangulation, snapshot, result);
}
};
if let InsertionLabel::Label(label) = label {
let set_label = triangulation.set_vertex_data(&subdivision.new_vertex, Some(label));
if let Err(err) = set_label {
let result = reject_backend(
BackendMutationOperation::SetVertexData,
format!("vertex {:?}", subdivision.new_vertex.vertex_key()),
&err,
);
return rollback_if_failed(triangulation, snapshot, result);
}
}
let result = self.finish_mutated_move(triangulation, MoveType::Move13Add);
rollback_if_failed(triangulation, snapshot, result)
}
fn apply_toroidal_insertion_site(
&mut self,
triangulation: &mut CdtTriangulation2D,
candidate: ToroidalInsertionCandidate,
) -> MoveResult {
let snapshot = triangulation.clone();
let subdivision_target = format!("face {:?}", candidate.face.simplex_key());
let subdivision = triangulation.subdivide_face(candidate.face, &candidate.point);
let subdivision = match subdivision {
Ok(subdivision) => subdivision,
Err(err) => {
let result = reject_backend(
BackendMutationOperation::SubdivideFace,
subdivision_target,
&err,
);
return rollback_if_failed(triangulation, snapshot, result);
}
};
if let Err(err) =
triangulation.set_vertex_data(&subdivision.new_vertex, Some(candidate.label))
{
let result = reject_backend(
BackendMutationOperation::SetVertexData,
format!("vertex {:?}", subdivision.new_vertex.vertex_key()),
&err,
);
return rollback_if_failed(triangulation, snapshot, result);
}
let flip_target = format!("{:?}", candidate.edge);
let flip_result = triangulation.flip_edge(candidate.edge);
let result = match flip_result {
Ok(_) => self.finish_mutated_move(triangulation, MoveType::Move13Add),
Err(err) => reject_backend(BackendMutationOperation::FlipEdge, flip_target, &err),
};
rollback_if_failed(triangulation, snapshot, result)
}
fn apply_vertex_removal_site(
&mut self,
triangulation: &mut CdtTriangulation2D,
vertex: DelaunayVertexHandle,
) -> MoveResult {
let removal_target = format!("vertex {:?}", vertex.vertex_key());
let snapshot = triangulation.clone();
let removal = triangulation.remove_vertex(vertex);
let result = match removal {
Ok(_) => self.finish_mutated_move(triangulation, MoveType::Move31Remove),
Err(err) => {
reject_backend(BackendMutationOperation::RemoveVertex, removal_target, &err)
}
};
rollback_if_failed(triangulation, snapshot, result)
}
fn apply_toroidal_removal_site(
&mut self,
triangulation: &mut CdtTriangulation2D,
candidate: ToroidalRemovalCandidate,
) -> MoveResult {
let snapshot = triangulation.clone();
let flip_target = format!("{:?}", candidate.flip_edge);
let flip_result = triangulation.flip_edge(candidate.flip_edge);
if let Err(err) = flip_result {
let result = reject_backend(BackendMutationOperation::FlipEdge, flip_target, &err);
return rollback_if_failed(triangulation, snapshot, result);
}
let removal_target = format!("vertex {:?}", candidate.vertex.vertex_key());
let removal = triangulation.remove_vertex(candidate.vertex);
let result = match removal {
Ok(_) => self.finish_mutated_move(triangulation, MoveType::Move31Remove),
Err(err) => {
reject_backend(BackendMutationOperation::RemoveVertex, removal_target, &err)
}
};
rollback_if_failed(triangulation, snapshot, result)
}
fn finish_mutated_move(
&mut self,
triangulation: &mut CdtTriangulation2D,
move_type: MoveType,
) -> MoveResult {
if let Err(err) = triangulation.synchronize_foliation_from_live_labels() {
return MoveResult::HardFailure(err);
}
if let Err(err) = triangulation.validate_evolved_cdt() {
return MoveResult::HardFailure(err);
}
self.stats.record_success(move_type);
MoveResult::Success
}
const fn record_hard_failure_if_needed(
&mut self,
move_type: MoveType,
result: MoveResult,
) -> MoveResult {
if matches!(result, MoveResult::HardFailure(_)) {
self.stats.record_hard_failure(move_type);
}
result
}
}
impl Default for ErgodicsSystem {
fn default() -> Self {
Self::new()
}
}
fn rollback_if_failed(
triangulation: &mut CdtTriangulation2D,
snapshot: CdtTriangulation2D,
result: MoveResult,
) -> MoveResult {
if matches!(result, MoveResult::Success) {
return result;
}
*triangulation = snapshot;
result
}
fn rejected_empty_selection(
triangulation: &CdtTriangulation2D,
selection: &ProposalSiteSelection,
) -> MoveResult {
if selection.geometric_candidate_seen && triangulation.has_foliation() {
MoveResult::CausalityViolation
} else {
MoveResult::GeometricViolation
}
}
fn reject_backend(
operation: BackendMutationOperation,
target: String,
err: impl Display,
) -> MoveResult {
MoveResult::Rejected(CdtError::BackendMutationFailed {
operation,
target,
detail: err.to_string(),
})
}
fn time_dist(triangulation: &CdtTriangulation2D, t0: u32, t1: u32) -> u32 {
let raw = t0.abs_diff(t1);
if matches!(triangulation.metadata().topology(), CdtTopology::Toroidal) {
let total = triangulation.time_slices().get();
if t0 < total && t1 < total {
return raw.min(total - raw);
}
}
raw
}
fn cdt_labels(triangulation: &CdtTriangulation2D, labels: [u32; 3]) -> bool {
let mut spacelike = 0;
let mut timelike = 0;
for (a, b) in [
(labels[0], labels[1]),
(labels[1], labels[2]),
(labels[2], labels[0]),
] {
match time_dist(triangulation, a, b) {
0 => spacelike += 1,
1 => timelike += 1,
_ => return false,
}
}
spacelike == 1 && timelike == 2
}
fn cdt_vertices(triangulation: &CdtTriangulation2D, vertices: &[DelaunayVertexHandle]) -> bool {
if !triangulation.has_foliation() {
return true;
}
let [v0, v1, v2] = vertices else {
return false;
};
cdt_vertex_triple(triangulation, [v0, v1, v2])
}
fn vertex_labels3(
triangulation: &CdtTriangulation2D,
vertices: [&DelaunayVertexHandle; 3],
) -> Option<[u32; 3]> {
let labels = vertices.map(|vertex| {
triangulation
.geometry()
.vertex_data_by_key(vertex.vertex_key())
});
let [Some(t0), Some(t1), Some(t2)] = labels else {
return None;
};
Some([t0, t1, t2])
}
fn cdt_vertex_triple(
triangulation: &CdtTriangulation2D,
vertices: [&DelaunayVertexHandle; 3],
) -> bool {
if !triangulation.has_foliation() {
return true;
}
vertex_labels3(triangulation, vertices).is_some_and(|labels| cdt_labels(triangulation, labels))
}
fn flip_is_causal(
triangulation: &CdtTriangulation2D,
adjacent: &EdgeAdjacentFaces<DelaunayVertexHandle, DelaunayFaceHandle>,
) -> bool {
let (endpoint_0, endpoint_1) = &adjacent.endpoints;
let (opposite_0, opposite_1) = &adjacent.opposite_vertices;
cdt_vertex_triple(triangulation, [endpoint_0, opposite_0, opposite_1])
&& cdt_vertex_triple(triangulation, [endpoint_1, opposite_0, opposite_1])
}
fn edge_flip_candidate_is_sampleable(
triangulation: &CdtTriangulation2D,
edge: &DelaunayEdgeHandle,
adjacent: &EdgeAdjacentFaces<DelaunayVertexHandle, DelaunayFaceHandle>,
) -> bool {
let (opposite_0, opposite_1) = &adjacent.opposite_vertices;
triangulation.geometry().can_flip_edge(edge)
&& flip_is_causal(triangulation, adjacent)
&& !edge_exists_between(triangulation, opposite_0, opposite_1)
}
fn insertion_label(
triangulation: &CdtTriangulation2D,
face: &DelaunayFaceHandle,
) -> Option<InsertionLabel> {
if !triangulation.has_foliation() {
return Some(InsertionLabel::Unfoliated);
}
causal_insertion_label(triangulation, face)
}
fn insertion_candidate_is_sampleable(point: &[f64; 2]) -> bool {
point.iter().all(|coordinate| coordinate.is_finite())
}
const fn toroidal_neighbor_labels(
triangulation: &CdtTriangulation2D,
label: u32,
) -> Option<(u32, u32)> {
let total = triangulation.time_slices().get();
if total < 3 || label >= total {
return None;
}
let previous = if label == 0 { total - 1 } else { label - 1 };
let next = (label + 1) % total;
Some((previous, next))
}
const fn labels_are_toroidal_neighbors(
triangulation: &CdtTriangulation2D,
base: u32,
first: u32,
second: u32,
) -> bool {
let Some((previous, next)) = toroidal_neighbor_labels(triangulation, base) else {
return false;
};
(first == previous && second == next) || (first == next && second == previous)
}
fn toroidal_insertion_candidate(
triangulation: &CdtTriangulation2D,
edge: DelaunayEdgeHandle,
adjacent: &EdgeAdjacentFaces<DelaunayVertexHandle, DelaunayFaceHandle>,
) -> Option<ToroidalInsertionCandidate> {
let (endpoint_0, endpoint_1) = &adjacent.endpoints;
let endpoint_0_label = triangulation
.geometry()
.vertex_data_by_key(endpoint_0.vertex_key())?;
let endpoint_1_label = triangulation
.geometry()
.vertex_data_by_key(endpoint_1.vertex_key())?;
if endpoint_0_label != endpoint_1_label {
return None;
}
let (opposite_0, opposite_1) = &adjacent.opposite_vertices;
let opposite_0_label = triangulation
.geometry()
.vertex_data_by_key(opposite_0.vertex_key())?;
let opposite_1_label = triangulation
.geometry()
.vertex_data_by_key(opposite_1.vertex_key())?;
if !labels_are_toroidal_neighbors(
triangulation,
endpoint_0_label,
opposite_0_label,
opposite_1_label,
) {
return None;
}
if !cdt_vertex_triple(triangulation, [endpoint_0, endpoint_1, opposite_0])
|| !cdt_vertex_triple(triangulation, [endpoint_0, endpoint_1, opposite_1])
{
return None;
}
let face = adjacent.faces.0.clone();
let point = centroid(triangulation, &face)?;
Some(ToroidalInsertionCandidate {
edge,
face,
point,
label: endpoint_0_label,
})
}
fn toroidal_insertion_candidate_is_sampleable(candidate: &ToroidalInsertionCandidate) -> bool {
candidate
.point
.iter()
.all(|coordinate| coordinate.is_finite())
}
fn causal_insertion_label(
triangulation: &CdtTriangulation2D,
face: &DelaunayFaceHandle,
) -> Option<InsertionLabel> {
let vertices = triangulation.geometry().face_vertices(face).ok()?;
let [v0, v1, v2] = vertices.as_slice() else {
return None;
};
let [t0, t1, t2] = vertex_labels3(triangulation, [v0, v1, v2])?;
let candidates = [t0, t1, t2];
for (index, candidate) in candidates.into_iter().enumerate() {
if candidates[..index].contains(&candidate) {
continue;
}
let valid = cdt_labels(triangulation, [candidate, t0, t1])
&& cdt_labels(triangulation, [candidate, t1, t2])
&& cdt_labels(triangulation, [candidate, t2, t0]);
if valid {
return Some(InsertionLabel::Label(candidate));
}
}
None
}
fn vertex_point_2d(
triangulation: &CdtTriangulation2D,
vertex: &DelaunayVertexHandle,
) -> Option<[f64; 2]> {
let coords = triangulation.geometry().vertex_coordinates(vertex).ok()?;
let [x, y] = coords.as_slice() else {
return None;
};
Some([*x, *y])
}
fn centroid(triangulation: &CdtTriangulation2D, face: &DelaunayFaceHandle) -> Option<[f64; 2]> {
let vertices = triangulation.geometry().face_vertices(face).ok()?;
let [v0, v1, v2] = vertices.as_slice() else {
return None;
};
let coords = [
vertex_point_2d(triangulation, v0)?,
vertex_point_2d(triangulation, v1)?,
vertex_point_2d(triangulation, v2)?,
];
if matches!(triangulation.metadata().topology(), CdtTopology::Toroidal) {
return toroidal_centroid(&coords, triangulation.geometry().periodic_domain()?);
}
Some([
(coords[0][0] + coords[1][0] + coords[2][0]) / 3.0,
(coords[0][1] + coords[1][1] + coords[2][1]) / 3.0,
])
}
fn toroidal_centroid(coords: &[[f64; 2]], domain: [f64; 2]) -> Option<[f64; 2]> {
let [reference, coord_1, coord_2] = coords else {
return None;
};
if domain
.iter()
.any(|period| !period.is_finite() || *period <= 0.0)
{
return None;
}
let mut centroid = *reference;
for coord in [coord_1, coord_2] {
for axis in 0..2 {
let period = domain[axis];
let mut unwrapped = coord[axis];
let delta = unwrapped - reference[axis];
if delta > period / 2.0 {
unwrapped -= period;
} else if delta < -period / 2.0 {
unwrapped += period;
}
centroid[axis] += unwrapped;
}
}
for axis in 0..2 {
centroid[axis] = (centroid[axis] / 3.0).rem_euclid(domain[axis]);
}
Some(centroid)
}
fn other_endpoint(
triangulation: &CdtTriangulation2D,
edge: &DelaunayEdgeHandle,
vertex: &DelaunayVertexHandle,
) -> Option<DelaunayVertexHandle> {
let (first, second) = triangulation.geometry().edge_endpoints(edge)?;
if &first == vertex {
Some(second)
} else if &second == vertex {
Some(first)
} else {
None
}
}
fn edge_exists_between(
triangulation: &CdtTriangulation2D,
first: &DelaunayVertexHandle,
second: &DelaunayVertexHandle,
) -> bool {
triangulation
.geometry()
.incident_edges(first)
.is_ok_and(|edges| {
edges.into_iter().any(|edge| {
triangulation
.geometry()
.edge_endpoints(&edge)
.is_some_and(|(left, right)| &left == second || &right == second)
})
})
}
fn face_exists_with_vertices(
triangulation: &CdtTriangulation2D,
vertices: &[DelaunayVertexHandle; 3],
) -> bool {
triangulation.geometry().faces().any(|face| {
triangulation
.geometry()
.face_vertices(&face)
.is_ok_and(|face_vertices| {
face_vertices.len() == 3
&& vertices.iter().all(|vertex| face_vertices.contains(vertex))
})
})
}
fn opposites_match_pair(
adjacent: &EdgeAdjacentFaces<DelaunayVertexHandle, DelaunayFaceHandle>,
first: &DelaunayVertexHandle,
second: &DelaunayVertexHandle,
) -> bool {
let (opposite_0, opposite_1) = &adjacent.opposite_vertices;
(opposite_0 == first && opposite_1 == second) || (opposite_0 == second && opposite_1 == first)
}
fn toroidal_removal_candidate(
triangulation: &CdtTriangulation2D,
vertex: DelaunayVertexHandle,
) -> Option<ToroidalRemovalCandidate> {
let label = triangulation
.geometry()
.vertex_data_by_key(vertex.vertex_key())?;
let slice = usize::try_from(label).ok()?;
if triangulation
.slice_sizes()
.get(slice)
.is_none_or(|&count| count <= 3)
{
return None;
}
let incident_edges = triangulation.geometry().incident_edges(&vertex).ok()?;
if incident_edges.len() != 4 {
return None;
}
let mut spacelike_neighbors: [Option<DelaunayVertexHandle>; 2] = array::from_fn(|_| None);
let mut timelike_neighbors: [Option<(DelaunayVertexHandle, DelaunayEdgeHandle, u32)>; 2] =
array::from_fn(|_| None);
let mut spacelike_count = 0;
let mut timelike_count = 0;
for edge in incident_edges {
let neighbor = other_endpoint(triangulation, &edge, &vertex)?;
let neighbor_label = triangulation
.geometry()
.vertex_data_by_key(neighbor.vertex_key())?;
match time_dist(triangulation, label, neighbor_label) {
0 => {
let slot = spacelike_neighbors.get_mut(spacelike_count)?;
*slot = Some(neighbor);
spacelike_count += 1;
}
1 => {
let slot = timelike_neighbors.get_mut(timelike_count)?;
*slot = Some((neighbor, edge, neighbor_label));
timelike_count += 1;
}
_ => return None,
}
}
let [Some(space_0), Some(space_1)] = spacelike_neighbors else {
return None;
};
let [
Some((_, time_edge_0, time_label_0)),
Some((_, time_edge_1, time_label_1)),
] = timelike_neighbors
else {
return None;
};
if !labels_are_toroidal_neighbors(triangulation, label, time_label_0, time_label_1) {
return None;
}
if edge_exists_between(triangulation, &space_0, &space_1) {
return None;
}
for edge in [&time_edge_0, &time_edge_1] {
let Ok(Some(adjacent)) = triangulation.geometry().edge_adjacent_faces(edge) else {
continue;
};
if opposites_match_pair(&adjacent, &space_0, &space_1) {
return Some(ToroidalRemovalCandidate {
vertex,
flip_edge: edge.clone(),
});
}
}
None
}
fn removal_candidate_is_sampleable(
triangulation: &CdtTriangulation2D,
neighbors: &[DelaunayVertexHandle; 3],
) -> bool {
!face_exists_with_vertices(triangulation, neighbors)
}
fn toroidal_removal_candidate_is_sampleable(
triangulation: &CdtTriangulation2D,
candidate: &ToroidalRemovalCandidate,
) -> bool {
triangulation.geometry().can_flip_edge(&candidate.flip_edge)
}
fn neighbors3(
triangulation: &CdtTriangulation2D,
vertex: &DelaunayVertexHandle,
) -> Option<[DelaunayVertexHandle; 3]> {
let adjacent_faces = triangulation.geometry().adjacent_faces(vertex).ok()?;
if adjacent_faces.len() != 3 {
return None;
}
let mut neighbors = [None, None, None];
let mut neighbor_count = 0;
for face in adjacent_faces {
for candidate in triangulation.geometry().face_vertices(&face).ok()? {
if &candidate == vertex
|| neighbors[..neighbor_count]
.iter()
.flatten()
.any(|seen| seen == &candidate)
{
continue;
}
let slot = neighbors.get_mut(neighbor_count)?;
if slot.is_some() {
return None;
}
*slot = Some(candidate);
neighbor_count += 1;
}
}
let [Some(v0), Some(v1), Some(v2)] = neighbors else {
return None;
};
Some([v0, v1, v2])
}
fn removal_candidate_is_causal(
triangulation: &CdtTriangulation2D,
vertex: &DelaunayVertexHandle,
neighbors: &[DelaunayVertexHandle; 3],
) -> bool {
if !cdt_vertices(triangulation, neighbors) {
return false;
}
if !triangulation.has_foliation() {
return true;
}
let Some(label) = triangulation
.geometry()
.vertex_data_by_key(vertex.vertex_key())
else {
return false;
};
let Ok(slice) = usize::try_from(label) else {
return false;
};
triangulation
.slice_sizes()
.get(slice)
.is_some_and(|&count| count > 1)
}
pub(crate) fn proposal_site_count(
triangulation: &CdtTriangulation2D,
move_type: MoveType,
) -> usize {
MoveSiteCache::default().site_count(triangulation, move_type)
}
fn visit_proposal_sites(
triangulation: &CdtTriangulation2D,
move_type: MoveType,
mut visit: impl FnMut(ProposalSite),
) -> bool {
match move_type {
MoveType::Move22 | MoveType::EdgeFlip => edge_flip_sites(triangulation, &mut visit),
MoveType::Move13Add => insertion_sites(triangulation, &mut visit),
MoveType::Move31Remove => removal_sites(triangulation, &mut visit),
}
}
fn edge_flip_sites(
triangulation: &CdtTriangulation2D,
visit: &mut impl FnMut(ProposalSite),
) -> bool {
let geometry = triangulation.geometry();
let mut geometric_candidate_seen = false;
for edge in geometry.edges() {
let Ok(Some(adjacent)) = geometry.edge_adjacent_faces(&edge) else {
continue;
};
geometric_candidate_seen = true;
if edge_flip_candidate_is_sampleable(triangulation, &edge, &adjacent) {
visit(ProposalSite::EdgeFlip(edge));
}
}
geometric_candidate_seen
}
fn insertion_sites(
triangulation: &CdtTriangulation2D,
visit: &mut impl FnMut(ProposalSite),
) -> bool {
if is_toroidal_foliated(triangulation) {
return toroidal_insertion_sites(triangulation, visit);
}
let mut geometric_candidate_seen = false;
for face in triangulation.geometry().faces() {
let Some(point) = centroid(triangulation, &face) else {
continue;
};
geometric_candidate_seen = true;
let Some(label) = insertion_label(triangulation, &face) else {
continue;
};
if insertion_candidate_is_sampleable(&point) {
visit(ProposalSite::FaceSubdivision { face, point, label });
}
}
geometric_candidate_seen
}
fn toroidal_insertion_sites(
triangulation: &CdtTriangulation2D,
visit: &mut impl FnMut(ProposalSite),
) -> bool {
let geometry = triangulation.geometry();
let mut geometric_candidate_seen = false;
for edge in geometry.edges() {
let Ok(Some(adjacent)) = geometry.edge_adjacent_faces(&edge) else {
continue;
};
geometric_candidate_seen = true;
let Some(insert) = toroidal_insertion_candidate(triangulation, edge, &adjacent) else {
continue;
};
if toroidal_insertion_candidate_is_sampleable(&insert) {
visit(ProposalSite::ToroidalInsertion(insert));
}
}
geometric_candidate_seen
}
fn removal_sites(triangulation: &CdtTriangulation2D, visit: &mut impl FnMut(ProposalSite)) -> bool {
if is_toroidal_foliated(triangulation) {
for vertex in triangulation.geometry().vertices() {
let Some(remove) = toroidal_removal_candidate(triangulation, vertex) else {
continue;
};
if toroidal_removal_candidate_is_sampleable(triangulation, &remove) {
visit(ProposalSite::ToroidalRemoval(remove));
}
}
return true;
}
let mut geometric_candidate_seen = false;
for vertex in triangulation.geometry().vertices() {
let Some(neighbors) = neighbors3(triangulation, &vertex) else {
continue;
};
geometric_candidate_seen = true;
if removal_candidate_is_causal(triangulation, &vertex, &neighbors)
&& removal_candidate_is_sampleable(triangulation, &neighbors)
{
visit(ProposalSite::VertexRemoval(vertex));
}
}
geometric_candidate_seen
}
fn is_toroidal_foliated(triangulation: &CdtTriangulation2D) -> bool {
matches!(triangulation.metadata().topology(), CdtTopology::Toroidal)
&& triangulation.has_foliation()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::errors::{CdtValidationCheck, CdtValidationFailure};
use crate::geometry::DelaunayBackend2D;
use crate::geometry::generators::{build_delaunay2_from_simplices, build_delaunay2_with_data};
use approx::assert_relative_eq;
use std::assert_matches;
use std::collections::HashSet;
fn single_triangle() -> CdtTriangulation2D {
let dt = build_delaunay2_with_data(&[([0.0, 0.0], 0), ([1.0, 0.0], 0), ([0.5, 1.0], 1)])
.expect("build labeled triangle");
let backend = DelaunayBackend2D::from_triangulation(dt)
.expect("test Delaunay triangle should validate");
CdtTriangulation2D::from_labeled_delaunay(backend, 2, 2).expect("wrap labeled triangle")
}
fn square_two_triangles() -> CdtTriangulation2D {
let dt = build_delaunay2_from_simplices(
&[
([0.0, 0.0], 0),
([1.0, 0.0], 0),
([0.0, 1.0], 1),
([1.0, 1.0], 1),
],
&[vec![0, 1, 2], vec![1, 3, 2]],
)
.expect("build square CDT");
let backend = DelaunayBackend2D::from_triangulation(dt)
.expect("test Delaunay square should validate");
CdtTriangulation2D::from_labeled_delaunay(backend, 2, 2).expect("wrap square CDT")
}
#[test]
fn test_move_statistics() {
let mut stats = MoveStatistics::new();
stats.record_attempt(MoveType::Move22);
stats.record_attempt(MoveType::Move22);
stats.record_success(MoveType::Move22);
assert_eq!(stats.moves_22_attempted, 2);
assert_eq!(stats.moves_22_accepted, 1);
assert_relative_eq!(stats.acceptance_rate(MoveType::Move22), 0.5);
}
#[test]
fn move_stats_variants() {
let mut stats = MoveStatistics::new();
for move_type in [
MoveType::Move22,
MoveType::Move13Add,
MoveType::Move31Remove,
MoveType::EdgeFlip,
] {
assert_relative_eq!(stats.acceptance_rate(move_type), 0.0);
stats.record_attempt(move_type);
stats.record_hard_failure(move_type);
assert_relative_eq!(stats.acceptance_rate(move_type), 0.0);
stats.record_attempt(move_type);
stats.record_success(move_type);
assert_relative_eq!(stats.acceptance_rate(move_type), 0.5);
}
assert_eq!(stats.moves_22_attempted, 2);
assert_eq!(stats.moves_13_attempted, 2);
assert_eq!(stats.moves_31_attempted, 2);
assert_eq!(stats.edge_flips_attempted, 2);
assert_eq!(stats.moves_22_accepted, 1);
assert_eq!(stats.moves_13_accepted, 1);
assert_eq!(stats.moves_31_accepted, 1);
assert_eq!(stats.edge_flips_accepted, 1);
assert_eq!(stats.moves_22_hard_failed, 1);
assert_eq!(stats.moves_13_hard_failed, 1);
assert_eq!(stats.moves_31_hard_failed, 1);
assert_eq!(stats.edge_flips_hard_failed, 1);
assert_eq!(stats.total_hard_failures(), 4);
assert_relative_eq!(stats.total_acceptance_rate(), 0.5);
}
#[test]
fn move_statistics_saturate_extreme_counters() {
let mut stats = MoveStatistics {
moves_22_attempted: u64::MAX,
moves_13_attempted: 1,
moves_31_attempted: 1,
edge_flips_attempted: 1,
moves_22_accepted: u64::MAX,
moves_13_accepted: 1,
moves_31_accepted: 1,
edge_flips_accepted: 1,
moves_22_hard_failed: u64::MAX,
moves_13_hard_failed: 1,
moves_31_hard_failed: 1,
edge_flips_hard_failed: 1,
};
stats.record_attempt(MoveType::Move22);
stats.record_success(MoveType::Move22);
stats.record_hard_failure(MoveType::Move22);
assert_eq!(stats.moves_22_attempted, u64::MAX);
assert_eq!(stats.moves_22_accepted, u64::MAX);
assert_eq!(stats.moves_22_hard_failed, u64::MAX);
assert_eq!(stats.total_attempted(), u64::MAX);
assert_eq!(stats.total_accepted(), u64::MAX);
assert_eq!(stats.total_hard_failures(), u64::MAX);
}
#[test]
fn move_statistics_saturate_each_move_type_counter() {
for move_type in [
MoveType::Move22,
MoveType::Move13Add,
MoveType::Move31Remove,
MoveType::EdgeFlip,
] {
let mut stats = MoveStatistics::new();
match move_type {
MoveType::Move22 => {
stats.moves_22_attempted = u64::MAX;
stats.moves_22_accepted = u64::MAX;
stats.moves_22_hard_failed = u64::MAX;
}
MoveType::Move13Add => {
stats.moves_13_attempted = u64::MAX;
stats.moves_13_accepted = u64::MAX;
stats.moves_13_hard_failed = u64::MAX;
}
MoveType::Move31Remove => {
stats.moves_31_attempted = u64::MAX;
stats.moves_31_accepted = u64::MAX;
stats.moves_31_hard_failed = u64::MAX;
}
MoveType::EdgeFlip => {
stats.edge_flips_attempted = u64::MAX;
stats.edge_flips_accepted = u64::MAX;
stats.edge_flips_hard_failed = u64::MAX;
}
}
stats.record_attempt(move_type);
stats.record_success(move_type);
stats.record_hard_failure(move_type);
let (attempted, accepted, hard_failed) = match move_type {
MoveType::Move22 => (
stats.moves_22_attempted,
stats.moves_22_accepted,
stats.moves_22_hard_failed,
),
MoveType::Move13Add => (
stats.moves_13_attempted,
stats.moves_13_accepted,
stats.moves_13_hard_failed,
),
MoveType::Move31Remove => (
stats.moves_31_attempted,
stats.moves_31_accepted,
stats.moves_31_hard_failed,
),
MoveType::EdgeFlip => (
stats.edge_flips_attempted,
stats.edge_flips_accepted,
stats.edge_flips_hard_failed,
),
};
assert_eq!(attempted, u64::MAX);
assert_eq!(accepted, u64::MAX);
assert_eq!(hard_failed, u64::MAX);
assert_relative_eq!(stats.acceptance_rate(move_type), 1.0);
}
}
#[test]
fn hard_failure_result_updates_stats_without_acceptance() {
let mut system = ErgodicsSystem::new();
system.stats.record_attempt(MoveType::Move13Add);
let result = system.record_hard_failure_if_needed(
MoveType::Move13Add,
MoveResult::HardFailure(CdtError::ValidationFailed {
check: CdtValidationCheck::ErgodicMoveCandidateGeometry,
failure: CdtValidationFailure::ErgodicMoveCandidateGeometry {
detail: "simulated hard failure".to_string(),
},
}),
);
assert_matches!(result, MoveResult::HardFailure(_));
assert_eq!(system.stats.moves_13_attempted, 1);
assert_eq!(system.stats.moves_13_accepted, 0);
assert_eq!(system.stats.moves_13_hard_failed, 1);
assert_relative_eq!(system.stats.acceptance_rate(MoveType::Move13Add), 0.0);
}
#[test]
fn move_statistics_defaults_hard_failures_for_legacy_payloads() {
let stats: MoveStatistics = serde_json::from_str(
r#"{
"moves_22_attempted": 1,
"moves_22_accepted": 1,
"moves_13_attempted": 2,
"moves_13_accepted": 0,
"moves_31_attempted": 3,
"moves_31_accepted": 0,
"edge_flips_attempted": 4,
"edge_flips_accepted": 1
}"#,
)
.expect("legacy move statistics should deserialize");
assert_eq!(stats.total_attempted(), 10);
assert_eq!(stats.total_accepted(), 2);
assert_eq!(stats.total_hard_failures(), 0);
}
#[test]
fn move_statistics_deserialization_rejects_impossible_counters() {
let error = serde_json::from_str::<MoveStatistics>(
r#"{
"moves_22_attempted": 1,
"moves_22_accepted": 1,
"moves_22_hard_failed": 1,
"moves_13_attempted": 0,
"moves_13_accepted": 0,
"moves_31_attempted": 0,
"moves_31_accepted": 0,
"edge_flips_attempted": 0,
"edge_flips_accepted": 0
}"#,
)
.expect_err("accepted plus hard failures above attempts should be rejected");
assert!(
error
.to_string()
.contains("accepted plus hard-failure counters"),
"serde error should explain move-counter invariant, got {error}"
);
}
#[test]
fn move_22_uses_real_tri() {
let mut system = ErgodicsSystem::new();
let mut triangulation = square_two_triangles();
let result = system.attempt_22_move(&mut triangulation);
assert_eq!(system.stats.moves_22_attempted, 1);
assert_eq!(result, MoveResult::Success);
assert_eq!(system.stats.moves_22_accepted, 1);
assert!(
triangulation
.geometry()
.triangulation()
.tds()
.is_valid()
.is_ok()
);
assert!(triangulation.validate_causality().is_ok());
}
#[test]
fn move_22_rejects_boundary_edge() {
let mut system = ErgodicsSystem::new();
let mut triangulation = single_triangle();
let counts_before = (
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
);
let result = system.attempt_22_move(&mut triangulation);
assert_eq!(result, MoveResult::GeometricViolation);
assert_eq!(system.stats.moves_22_attempted, 1);
assert_eq!(system.stats.moves_22_accepted, 0);
assert_eq!(
(
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
),
counts_before
);
}
#[test]
fn move_13_inserts_labeled_vertex() {
let mut system = ErgodicsSystem::new();
let mut triangulation = single_triangle();
let before_vertices = triangulation.vertex_count();
let result = system.attempt_13_move(&mut triangulation);
assert_eq!(system.stats.moves_13_attempted, 1);
assert_eq!(result, MoveResult::Success);
assert_eq!(triangulation.vertex_count(), before_vertices + 1);
assert!(
triangulation
.geometry()
.triangulation()
.tds()
.is_valid()
.is_ok()
);
assert!(triangulation.validate_causality().is_ok());
assert!(triangulation.has_foliation());
}
#[test]
fn rollback_restores_snapshot_on_hard_failure() {
let mut triangulation = single_triangle();
let snapshot = triangulation.clone();
let counts_before = (
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
triangulation.metadata().modification_count(),
);
let face = triangulation
.geometry()
.faces()
.next()
.expect("triangle face");
let point = centroid(&triangulation, &face).expect("triangle centroid");
triangulation
.subdivide_face(face, &point)
.expect("subdivide fixture face");
assert_ne!(triangulation.vertex_count(), counts_before.0);
let result = rollback_if_failed(
&mut triangulation,
snapshot,
MoveResult::HardFailure(CdtError::ValidationFailed {
check: CdtValidationCheck::ErgodicMoveCandidateGeometry,
failure: CdtValidationFailure::ErgodicMoveCandidateGeometry {
detail: "simulated post-mutation failure".to_string(),
},
}),
);
assert_matches!(result, MoveResult::HardFailure(_));
assert_eq!(
(
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
triangulation.metadata().modification_count(),
),
counts_before
);
assert!(triangulation.validate().is_ok());
}
#[test]
fn unwraps_toroidal_centroid() {
let point = toroidal_centroid(&[[0.0, 0.0], [3.0, 0.0], [3.0, 1.0]], [4.0, 3.0])
.expect("toroidal centroid");
assert_relative_eq!(point[0], 10.0 / 3.0, epsilon = 1e-12);
assert_relative_eq!(point[1], 1.0 / 3.0, epsilon = 1e-12);
}
#[test]
fn toroidal_centroid_wraps_across_both_periodic_seams() {
let point = toroidal_centroid(&[[3.9, 2.9], [0.1, 2.8], [0.2, 0.1]], [4.0, 3.0])
.expect("toroidal centroid across both seams");
assert_relative_eq!(point[0], 0.066_666_666_666_666_43, epsilon = 1e-12);
assert_relative_eq!(point[1], 2.933_333_333_333_333, epsilon = 1e-12);
}
#[test]
fn toroidal_centroid_handles_half_period_ties_deterministically() {
let point = toroidal_centroid(&[[0.0, 0.0], [2.0, 0.0], [2.0, 1.5]], [4.0, 3.0])
.expect("half-period centroid should remain defined");
assert_relative_eq!(point[0], 4.0 / 3.0, epsilon = 1e-12);
assert_relative_eq!(point[1], 0.5, epsilon = 1e-12);
}
#[test]
fn checked_toroidal_wrapper_rejects_topology_mismatch() {
let dt = build_delaunay2_with_data(&[([0.0, 0.0], 0), ([1.0, 0.0], 0), ([0.5, 1.0], 1)])
.expect("build labeled triangle");
let backend = DelaunayBackend2D::from_triangulation(dt)
.expect("test Delaunay triangle should validate");
let result = CdtTriangulation2D::with_topology(backend, 3, 2, CdtTopology::Toroidal);
assert_matches!(
result,
Err(CdtError::TopologyMismatch {
topology,
euler_characteristic: 1,
expected_euler_characteristics,
..
}) if topology == CdtTopology::Toroidal && expected_euler_characteristics == [0]
);
}
#[test]
fn proposal_site_count_matches_open_boundary_move_availability() {
let mut system = ErgodicsSystem::new();
let mut triangulation = single_triangle();
assert_eq!(proposal_site_count(&triangulation, MoveType::Move22), 0);
assert_eq!(proposal_site_count(&triangulation, MoveType::EdgeFlip), 0);
assert_eq!(proposal_site_count(&triangulation, MoveType::Move13Add), 1);
assert_eq!(
proposal_site_count(&triangulation, MoveType::Move31Remove),
0
);
let insert = system.attempt_13_move(&mut triangulation);
assert_eq!(insert, MoveResult::Success);
assert_eq!(proposal_site_count(&triangulation, MoveType::Move13Add), 3);
assert_eq!(
proposal_site_count(&triangulation, MoveType::Move31Remove),
1
);
let removal = system.attempt_31_move(&mut triangulation);
assert_eq!(removal, MoveResult::Success);
assert_eq!(proposal_site_count(&triangulation, MoveType::Move13Add), 1);
assert_eq!(
proposal_site_count(&triangulation, MoveType::Move31Remove),
0
);
}
#[test]
fn selected_proposal_site_reports_count_and_empty_state() {
let mut system = ErgodicsSystem::with_seed(11);
let mut triangulation = single_triangle();
let empty_selection = system.select_proposal_site(&triangulation, MoveType::Move31Remove);
assert_eq!(empty_selection.site_count, 0);
assert!(empty_selection.site.is_none());
let insertion_selection = system.select_proposal_site(&triangulation, MoveType::Move13Add);
assert_eq!(
insertion_selection.site_count,
proposal_site_count(&triangulation, MoveType::Move13Add)
);
let Some(insertion_site) = insertion_selection.site else {
panic!("nonempty insertion selection should include a concrete site");
};
let result =
system.apply_proposal_site(&mut triangulation, MoveType::Move13Add, insertion_site);
assert_eq!(result, MoveResult::Success);
let removal_selection = system.select_proposal_site(&triangulation, MoveType::Move31Remove);
assert_eq!(
removal_selection.site_count,
proposal_site_count(&triangulation, MoveType::Move31Remove)
);
assert!(removal_selection.site.is_some());
}
#[test]
fn proposal_site_cache_records_no_site_self_loops_without_mutating() {
let mut system = ErgodicsSystem::with_seed(11);
let triangulation = single_triangle();
let counts_before = (
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
triangulation.metadata().modification_count(),
);
let empty_selection = system.select_proposal_site(&triangulation, MoveType::Move31Remove);
assert_eq!(empty_selection.site_count, 0);
assert!(empty_selection.site.is_none());
let cached_family = system.site_cache.family(MoveType::Move31Remove);
assert_eq!(cached_family.instance_id, Some(triangulation.instance_id()));
assert_eq!(
cached_family.modification_count,
Some(triangulation.metadata().modification_count())
);
let retry_selection = system.select_proposal_site(&triangulation, MoveType::Move31Remove);
assert_eq!(retry_selection.site_count, 0);
assert!(retry_selection.site.is_none());
assert_eq!(
(
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
triangulation.metadata().modification_count(),
),
counts_before
);
assert_eq!(
system
.site_cache
.family(MoveType::Move31Remove)
.modification_count,
Some(counts_before.3)
);
}
#[test]
fn proposal_site_cache_refreshes_after_accepted_mutation() {
let mut system = ErgodicsSystem::with_seed(11);
let mut triangulation = single_triangle();
let initial_modification_count = triangulation.metadata().modification_count();
let insertion_selection = system.select_proposal_site(&triangulation, MoveType::Move13Add);
assert_eq!(
system
.site_cache
.family(MoveType::Move13Add)
.modification_count,
Some(initial_modification_count)
);
assert_eq!(insertion_selection.site_count, 1);
let Some(insertion_site) = insertion_selection.site else {
panic!("single triangle should expose one insertion site");
};
let result =
system.apply_proposal_site(&mut triangulation, MoveType::Move13Add, insertion_site);
assert_eq!(result, MoveResult::Success);
let mutated_modification_count = triangulation.metadata().modification_count();
assert!(mutated_modification_count > initial_modification_count);
assert_eq!(
system
.site_cache
.family(MoveType::Move13Add)
.modification_count,
Some(initial_modification_count),
"accepted mutations leave the old cache stale until the next selection"
);
let removal_selection = system.select_proposal_site(&triangulation, MoveType::Move31Remove);
assert_eq!(
system
.site_cache
.family(MoveType::Move31Remove)
.modification_count,
Some(mutated_modification_count)
);
assert_eq!(
removal_selection.site_count,
proposal_site_count(&triangulation, MoveType::Move31Remove)
);
assert!(removal_selection.site.is_some());
}
#[test]
fn proposal_site_cache_remains_current_after_ordinary_rejection() {
let mut system = ErgodicsSystem::with_seed(11);
let mut triangulation = single_triangle();
let counts_before = (
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
triangulation.metadata().modification_count(),
);
let insertion_selection = system.select_proposal_site(&triangulation, MoveType::Move13Add);
let Some(insertion_site) = insertion_selection.site else {
panic!("single triangle should expose one insertion site");
};
let cached_modification_count = system
.site_cache
.family(MoveType::Move13Add)
.modification_count;
let result =
system.apply_proposal_site(&mut triangulation, MoveType::Move22, insertion_site);
assert_eq!(result, MoveResult::GeometricViolation);
assert_eq!(
(
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
triangulation.metadata().modification_count(),
),
counts_before
);
let retry_selection = system.select_proposal_site(&triangulation, MoveType::Move13Add);
assert_eq!(
system
.site_cache
.family(MoveType::Move13Add)
.modification_count,
cached_modification_count
);
assert_eq!(retry_selection.site_count, insertion_selection.site_count);
assert!(retry_selection.site.is_some());
}
#[test]
fn proposal_site_cache_tracks_cloned_proposed_states_by_modification_count() {
let mut system = ErgodicsSystem::with_seed(11);
let original = single_triangle();
let insertion_selection = system.select_proposal_site(&original, MoveType::Move13Add);
assert_eq!(
system
.site_cache
.family(MoveType::Move13Add)
.modification_count,
Some(original.metadata().modification_count())
);
let Some(insertion_site) = insertion_selection.site else {
panic!("single triangle should expose one insertion site");
};
let mut proposed_state = original.clone();
let result =
system.apply_proposal_site(&mut proposed_state, MoveType::Move13Add, insertion_site);
assert_eq!(result, MoveResult::Success);
assert_ne!(
proposed_state.metadata().modification_count(),
original.metadata().modification_count()
);
let proposed_selection =
system.select_proposal_site(&proposed_state, MoveType::Move31Remove);
assert_eq!(
system
.site_cache
.family(MoveType::Move31Remove)
.modification_count,
Some(proposed_state.metadata().modification_count())
);
assert!(proposed_selection.site.is_some());
let original_selection = system.select_proposal_site(&original, MoveType::Move13Add);
assert_eq!(
system
.site_cache
.family(MoveType::Move13Add)
.modification_count,
Some(original.metadata().modification_count())
);
assert_eq!(
original_selection.site_count,
insertion_selection.site_count
);
}
#[test]
fn proposal_site_cache_refreshes_for_distinct_triangulation_sources() {
let mut system = ErgodicsSystem::with_seed(11);
let first = single_triangle();
let second = single_triangle();
assert_eq!(
first.metadata().modification_count(),
second.metadata().modification_count()
);
let first_selection = system.select_proposal_site(&first, MoveType::Move13Add);
let first_instance_id = system
.site_cache
.family(MoveType::Move13Add)
.instance_id
.expect("selection should populate cache instance identity");
assert_eq!(first_selection.site_count, 1);
let second_selection = system.select_proposal_site(&second, MoveType::Move13Add);
assert_eq!(
system.site_cache.family(MoveType::Move13Add).instance_id,
Some(second.instance_id())
);
assert_ne!(
first_instance_id,
system
.site_cache
.family(MoveType::Move13Add)
.instance_id
.expect("second selection should populate cache instance identity")
);
assert_eq!(second_selection.site_count, first_selection.site_count);
}
#[test]
fn proposal_site_cache_refreshes_for_cloned_triangulation_instances() {
let mut system = ErgodicsSystem::with_seed(11);
let original = single_triangle();
let cloned = original.clone();
assert_eq!(
original.metadata().modification_count(),
cloned.metadata().modification_count()
);
assert_ne!(original.instance_id(), cloned.instance_id());
let original_selection = system.select_proposal_site(&original, MoveType::Move13Add);
assert_eq!(
system.site_cache.family(MoveType::Move13Add).instance_id,
Some(original.instance_id())
);
assert_eq!(original_selection.site_count, 1);
let cloned_selection = system.select_proposal_site(&cloned, MoveType::Move13Add);
assert_eq!(
system.site_cache.family(MoveType::Move13Add).instance_id,
Some(cloned.instance_id())
);
assert_eq!(cloned_selection.site_count, original_selection.site_count);
}
#[test]
fn mismatched_proposal_site_rejects_without_mutating() {
let mut system = ErgodicsSystem::with_seed(11);
let mut triangulation = single_triangle();
let counts_before = (
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
triangulation.metadata().modification_count(),
);
let insertion_selection = system.select_proposal_site(&triangulation, MoveType::Move13Add);
let Some(insertion_site) = insertion_selection.site else {
panic!("single triangle should expose one insertion site");
};
let result =
system.apply_proposal_site(&mut triangulation, MoveType::Move22, insertion_site);
assert_eq!(result, MoveResult::GeometricViolation);
assert_eq!(
(
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
triangulation.metadata().modification_count(),
),
counts_before
);
triangulation
.validate()
.expect("rejecting a mismatched proposal site should leave CDT invariants intact");
}
#[test]
fn proposal_site_count_handles_nonuniform_toroidal_profiles() {
let mut system = ErgodicsSystem::with_seed(7);
let mut triangulation = CdtTriangulation2D::from_toroidal_cdt_profile(&[3, 4, 5, 4])
.expect("nonuniform toroidal CDT should build");
let initial_insertions = proposal_site_count(&triangulation, MoveType::Move13Add);
let initial_removals = proposal_site_count(&triangulation, MoveType::Move31Remove);
assert!(
initial_insertions > 0,
"nonuniform toroidal profiles should expose counted insertion sites"
);
assert!(
initial_removals > 0,
"nonuniform toroidal profiles should expose counted inverse-volume sites"
);
let result = system.attempt_13_move(&mut triangulation);
assert_eq!(result, MoveResult::Success);
triangulation
.validate()
.expect("counted nonuniform-profile insertion should preserve CDT invariants");
assert!(proposal_site_count(&triangulation, MoveType::Move13Add) > 0);
}
#[test]
fn removal_sampleable_guard_rejects_existing_replacement_face() {
let triangulation = single_triangle();
let face = triangulation
.geometry()
.faces()
.next()
.expect("triangle fixture should have one face");
let vertices = triangulation
.geometry()
.face_vertices(&face)
.expect("triangle face should resolve vertices");
let [v0, v1, v2] = vertices.as_slice() else {
panic!("triangle fixture face should have exactly three vertices");
};
let replacement_vertices = [v0.clone(), v1.clone(), v2.clone()];
assert!(face_exists_with_vertices(
&triangulation,
&replacement_vertices
));
assert!(!removal_candidate_is_sampleable(
&triangulation,
&replacement_vertices
));
}
#[test]
fn move_31_removes_degree_three() {
let mut system = ErgodicsSystem::new();
let mut triangulation = single_triangle();
let result = system.attempt_13_move(&mut triangulation);
assert_matches!(result, MoveResult::Success);
let before_vertices = triangulation.vertex_count();
let result = system.attempt_31_move(&mut triangulation);
assert_eq!(system.stats.moves_31_attempted, 1);
assert_eq!(result, MoveResult::Success);
assert_eq!(triangulation.vertex_count(), before_vertices - 1);
assert!(
triangulation
.geometry()
.triangulation()
.tds()
.is_valid()
.is_ok()
);
assert!(triangulation.validate_causality().is_ok());
}
#[test]
fn move_31_requires_degree_three() {
let mut system = ErgodicsSystem::new();
let mut triangulation = single_triangle();
let counts_before = (
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
);
let result = system.attempt_31_move(&mut triangulation);
assert_eq!(result, MoveResult::GeometricViolation);
assert_eq!(system.stats.moves_31_attempted, 1);
assert_eq!(system.stats.moves_31_accepted, 0);
assert_eq!(
(
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
),
counts_before
);
}
#[test]
fn periodic_toroidal_move_13_splits_spacelike_link() {
let mut system = ErgodicsSystem::with_seed(7);
let mut triangulation =
CdtTriangulation2D::from_toroidal_cdt(8, 8).expect("build toroidal CDT");
let counts_before = (
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
);
let result = system.attempt_13_move(&mut triangulation);
assert_eq!(
result,
MoveResult::Success,
"periodic toroidal Move13Add should split a spacelike link, got {result:?}"
);
assert_eq!(system.stats.moves_13_accepted, 1);
assert_eq!(
(
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
),
(
counts_before.0 + 1,
counts_before.1 + 3,
counts_before.2 + 2
),
"accepted periodic toroidal insertion should apply the CDT volume-move count delta"
);
triangulation
.validate()
.expect("accepted periodic toroidal Move13Add should preserve evolved CDT invariants");
}
#[test]
fn periodic_toroidal_move_31_reverses_link_split() {
let mut system = ErgodicsSystem::with_seed(0);
let mut triangulation =
CdtTriangulation2D::from_toroidal_cdt(8, 8).expect("build toroidal CDT");
let insert = system.attempt_13_move(&mut triangulation);
assert_eq!(insert, MoveResult::Success);
let counts_before = (
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
);
let mut removed = false;
for _ in 0..64 {
match system.attempt_31_move(&mut triangulation) {
MoveResult::Success => {
removed = true;
break;
}
MoveResult::Rejected(_) | MoveResult::GeometricViolation => {}
other => panic!("unexpected toroidal removal result: {other:?}"),
}
}
assert!(
removed,
"sampleable periodic toroidal Move31Remove sites should include upstream offset-aware successes"
);
assert_eq!(system.stats.moves_31_accepted, 1);
assert_eq!(
(
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
),
(
counts_before.0 - 1,
counts_before.1 - 3,
counts_before.2 - 2
),
"accepted periodic toroidal removal should reverse a local 1,3 subdivision"
);
assert!(
triangulation
.volume_profile()
.iter()
.all(|&count| count >= 3),
"accepted periodic toroidal removal must preserve nonempty closed spatial slices"
);
triangulation.validate().expect(
"accepted periodic toroidal Move31Remove should preserve evolved CDT invariants",
);
}
#[test]
fn proposal_site_count_tracks_toroidal_inverse_sites() {
let mut system = ErgodicsSystem::with_seed(0);
let mut triangulation =
CdtTriangulation2D::from_toroidal_cdt(8, 8).expect("build toroidal CDT");
assert!(
proposal_site_count(&triangulation, MoveType::Move31Remove) > 0,
"initial toroidal CDT should expose sampleable inverse-volume sites"
);
assert!(proposal_site_count(&triangulation, MoveType::Move13Add) > 0);
let mut inserted = false;
for _ in 0..64 {
match system.attempt_13_move(&mut triangulation) {
MoveResult::Success => {
inserted = true;
break;
}
MoveResult::Rejected(_) | MoveResult::CausalityViolation => {}
other => panic!("unexpected toroidal insertion result: {other:?}"),
}
}
assert!(
inserted,
"sampleable toroidal insertion sites should include successes"
);
assert!(
proposal_site_count(&triangulation, MoveType::Move31Remove) > 0,
"a toroidal spacelike-link split should expose at least one sampleable inverse site"
);
let mut removed = false;
for _ in 0..64 {
match system.attempt_31_move(&mut triangulation) {
MoveResult::Success => {
removed = true;
break;
}
MoveResult::Rejected(_) | MoveResult::GeometricViolation => {}
other => panic!("unexpected toroidal removal result: {other:?}"),
}
}
assert!(
removed,
"sampleable toroidal inverse sites should include successes"
);
triangulation
.validate()
.expect("proposal-counted toroidal inverse move should preserve CDT invariants");
}
#[test]
fn periodic_toroidal_move_31_rejects_minimal_slice_removal() {
let mut system = ErgodicsSystem::with_seed(7);
let mut triangulation =
CdtTriangulation2D::from_toroidal_cdt(3, 3).expect("build minimal toroidal CDT");
let counts_before = (
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
);
let profile_before = triangulation.volume_profile();
let result = system.attempt_31_move(&mut triangulation);
assert_matches!(
result,
MoveResult::CausalityViolation,
"minimal periodic toroidal slices should reject volume removal causally"
);
assert_eq!(system.stats.moves_31_attempted, 1);
assert_eq!(system.stats.moves_31_accepted, 0);
assert_eq!(
(
triangulation.vertex_count(),
triangulation.edge_count(),
triangulation.face_count(),
),
counts_before,
"rejected minimal toroidal removal must preserve simplex counts"
);
assert_eq!(
triangulation.volume_profile(),
profile_before,
"rejected minimal toroidal removal must preserve closed spatial slices"
);
triangulation
.validate()
.expect("rejected minimal toroidal removal should preserve CDT invariants");
}
#[test]
fn periodic_toroidal_k2_move_attempts_preserve_invariants() {
type AttemptK2Move = fn(&mut ErgodicsSystem, &mut CdtTriangulation2D) -> MoveResult;
type K2MoveCase = (MoveType, u64, AttemptK2Move);
let cases: [K2MoveCase; 2] = [
(MoveType::Move22, 11, ErgodicsSystem::attempt_22_move),
(MoveType::EdgeFlip, 13, ErgodicsSystem::attempt_edge_flip),
];
for (move_type, seed, attempt_move) in cases {
let mut system = ErgodicsSystem::with_seed(seed);
let mut triangulation =
CdtTriangulation2D::from_toroidal_cdt(8, 8).expect("build toroidal CDT");
let result = attempt_move(&mut system, &mut triangulation);
assert_matches!(
result,
MoveResult::Success
| MoveResult::CausalityViolation
| MoveResult::GeometricViolation,
"periodic toroidal {move_type:?} should not fail through backend offset handling"
);
triangulation.validate().expect(
"periodic toroidal k=2 move attempt should preserve evolved CDT invariants",
);
}
}
#[test]
fn edge_flip_uses_own_stats() {
let mut system = ErgodicsSystem::new();
let mut triangulation = square_two_triangles();
let result = system.attempt_edge_flip(&mut triangulation);
assert_eq!(system.stats.edge_flips_attempted, 1);
assert_eq!(system.stats.moves_22_attempted, 0);
assert_eq!(result, MoveResult::Success);
assert_eq!(system.stats.edge_flips_accepted, 1);
assert!(triangulation.validate_causality().is_ok());
}
#[test]
fn test_random_move_selection() {
let mut system = ErgodicsSystem::new();
let mut move_types = HashSet::new();
for _ in 0..100 {
move_types.insert(system.select_random_move());
}
assert!(move_types.len() > 1);
}
#[test]
fn test_total_acceptance_rate() {
let mut stats = MoveStatistics::new();
stats.record_attempt(MoveType::Move22);
stats.record_success(MoveType::Move22);
stats.record_attempt(MoveType::Move13Add);
assert_relative_eq!(stats.total_acceptance_rate(), 0.5);
}
#[test]
fn total_acceptance_no_attempts() {
let stats = MoveStatistics::new();
assert_relative_eq!(stats.total_acceptance_rate(), 0.0);
}
}