#![forbid(unsafe_code)]
use crate::core::{
collections::{
FacetToSimplicesMap, FastHashMap, FastHashSet, FastHasher, SimplexKeySet, SmallBuffer,
VertexKeyBuffer, fast_hash_map_with_capacity, fast_hash_set_with_capacity,
},
facet::{FacetHandle, facet_key_from_vertices},
tds::{SimplexKey, Tds, TdsError, VertexKey},
};
use crate::topology::characteristics::euler::{
triangulated_surface_boundary_component_count, triangulated_surface_euler_characteristic,
};
use slotmap::Key;
use std::{
cmp::Ordering,
hash::{Hash, Hasher},
};
use thiserror::Error;
#[derive(Clone, Debug, Eq, PartialEq)]
struct LiftedVertexId {
vertex_key: VertexKey,
offset: SmallBuffer<i16, 8>,
}
type LiftedVertexBuffer = SmallBuffer<LiftedVertexId, 8>;
type LinkSimplexBuffer = SmallBuffer<LiftedVertexBuffer, 8>;
impl LiftedVertexId {
fn base(vertex_key: VertexKey) -> Self {
Self {
vertex_key,
offset: SmallBuffer::new(),
}
}
fn is_base(&self) -> bool {
self.offset.is_empty()
}
}
impl Ord for LiftedVertexId {
fn cmp(&self, other: &Self) -> Ordering {
self.vertex_key
.data()
.as_ffi()
.cmp(&other.vertex_key.data().as_ffi())
.then_with(|| self.offset.as_slice().cmp(other.offset.as_slice()))
}
}
impl PartialOrd for LiftedVertexId {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Hash for LiftedVertexId {
fn hash<H: Hasher>(&self, state: &mut H) {
self.vertex_key.data().as_ffi().hash(state);
self.offset.as_slice().hash(state);
}
}
fn lifted_vertex_id<O>(vk: VertexKey, offset: &[O]) -> LiftedVertexId
where
O: Copy + Into<i16>,
{
if offset.is_empty() || offset.iter().all(|&o| o.into() == 0) {
return LiftedVertexId::base(vk);
}
LiftedVertexId {
vertex_key: vk,
offset: offset.iter().map(|&component| component.into()).collect(),
}
}
fn periodic_simplex_key(lifted_vertices: &[LiftedVertexId]) -> u64 {
if lifted_vertices.iter().all(LiftedVertexId::is_base) {
let bare_vertices: VertexKeyBuffer =
lifted_vertices.iter().map(|id| id.vertex_key).collect();
return facet_key_from_vertices(&bare_vertices);
}
let keys = normalize_lifted_vertices(lifted_vertices);
let mut hasher = FastHasher::default();
for key in &keys {
key.hash(&mut hasher);
}
hasher.finish()
}
fn anchored_lifted_simplex_key(lifted_vertices: &[LiftedVertexId]) -> u64 {
if lifted_vertices.iter().all(LiftedVertexId::is_base) {
let bare_vertices: VertexKeyBuffer =
lifted_vertices.iter().map(|id| id.vertex_key).collect();
return facet_key_from_vertices(&bare_vertices);
}
let mut keys: LiftedVertexBuffer = lifted_vertices.iter().cloned().collect();
keys.sort_unstable();
let mut hasher = FastHasher::default();
for key in &keys {
key.hash(&mut hasher);
}
hasher.finish()
}
fn normalize_lifted_vertices(lifted_vertices: &[LiftedVertexId]) -> LiftedVertexBuffer {
let mut keys: LiftedVertexBuffer = lifted_vertices.iter().cloned().collect();
keys.sort_unstable();
let anchor_offset: SmallBuffer<i16, 8> = keys
.first()
.map_or_else(SmallBuffer::new, |key| key.offset.clone());
let axes = keys
.iter()
.map(|key| key.offset.len())
.max()
.unwrap_or(0)
.max(anchor_offset.len());
let mut normalized = LiftedVertexBuffer::with_capacity(keys.len());
for key in keys {
let mut offset: SmallBuffer<i16, 8> = SmallBuffer::with_capacity(axes);
for axis in 0..axes {
let component = key.offset.get(axis).copied().unwrap_or(0)
- anchor_offset.get(axis).copied().unwrap_or(0);
offset.push(component);
}
normalized.push(lifted_vertex_id(key.vertex_key, &offset));
}
normalized
}
fn ordered_lifted_edge(a: &LiftedVertexId, b: &LiftedVertexId) -> (LiftedVertexId, LiftedVertexId) {
if b < a {
(b.clone(), a.clone())
} else {
(a.clone(), b.clone())
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ManifoldError {
#[error(transparent)]
Tds(#[from] TdsError),
#[error(
"Non-manifold facet: facet {facet_key:016x} belongs to {simplex_count} simplices (expected 1 or 2)"
)]
ManifoldFacetMultiplicity {
facet_key: u64,
simplex_count: usize,
},
#[error(
"Boundary is not closed: boundary ridge {ridge_key:016x} is incident to {boundary_facet_count} boundary facets (expected 2)"
)]
BoundaryRidgeMultiplicity {
ridge_key: u64,
boundary_facet_count: usize,
},
#[error(
"Ridge link is not a 1-manifold: ridge {ridge_key:016x} has link graph with {link_vertex_count} vertices, {link_edge_count} edges, max degree {max_degree}, degree-1 vertices {degree_one_vertices}, connected={connected} (expected connected cycle or path)"
)]
RidgeLinkNotManifold {
ridge_key: u64,
link_vertex_count: usize,
link_edge_count: usize,
max_degree: usize,
degree_one_vertices: usize,
connected: bool,
},
#[error(
"Vertex link is not a PL (D-1)-manifold: vertex {vertex_key:?} has link with {link_vertex_count} vertices, {link_simplex_count} simplices, boundary_facets={boundary_facet_count}, max_degree={max_degree}, connected={connected}, interior_vertex={interior_vertex}"
)]
VertexLinkNotManifold {
vertex_key: VertexKey,
link_vertex_count: usize,
link_simplex_count: usize,
boundary_facet_count: usize,
max_degree: usize,
connected: bool,
interior_vertex: bool,
},
}
pub fn validate_facet_degree(
facet_to_simplices: &FacetToSimplicesMap,
) -> Result<(), ManifoldError> {
for (facet_key, simplex_facet_pairs) in facet_to_simplices {
match simplex_facet_pairs.as_slice() {
[_] | [_, _] => {}
_ => {
return Err(ManifoldError::ManifoldFacetMultiplicity {
facet_key: *facet_key,
simplex_count: simplex_facet_pairs.len(),
});
}
}
}
Ok(())
}
pub fn validate_closed_boundary<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
facet_to_simplices: &FacetToSimplicesMap,
) -> Result<(), ManifoldError> {
if D < 2 {
return Ok(());
}
let mut boundary_facet_count = 0usize;
for simplex_facet_pairs in facet_to_simplices.values() {
let [handle] = simplex_facet_pairs.as_slice() else {
continue;
};
if is_boundary_facet_handle(tds, *handle)? {
boundary_facet_count = boundary_facet_count.saturating_add(1);
}
}
if boundary_facet_count == 0 {
return Ok(());
}
let estimated_boundary_ridges = boundary_facet_count
.saturating_mul(D)
.saturating_div(2)
.max(1);
let mut ridge_to_boundary_facet_count: FastHashMap<u64, usize> =
fast_hash_map_with_capacity(estimated_boundary_ridges);
let mut facet_vertices: VertexKeyBuffer = VertexKeyBuffer::with_capacity(D);
let mut ridge_vertices: VertexKeyBuffer = VertexKeyBuffer::with_capacity(D.saturating_sub(1));
for simplex_facet_pairs in facet_to_simplices.values() {
let [handle] = simplex_facet_pairs.as_slice() else {
continue;
};
let simplex_key = handle.simplex_key();
let facet_index = handle.facet_index() as usize;
if !is_boundary_facet_handle(tds, *handle)? {
continue;
}
let simplex_vertices = tds.simplex_vertices(simplex_key)?;
if facet_index >= simplex_vertices.len() {
return Err(TdsError::IndexOutOfBounds {
index: facet_index,
bound: simplex_vertices.len(),
context: format!("boundary facet index for simplex {simplex_key:?}"),
}
.into());
}
facet_vertices.clear();
for (i, &vk) in simplex_vertices.iter().enumerate() {
if i == facet_index {
continue;
}
facet_vertices.push(vk);
}
if facet_vertices.len() != D {
return Err(TdsError::DimensionMismatch {
expected: D,
actual: facet_vertices.len(),
context: format!(
"boundary facet vertex count (simplex_key={simplex_key:?}, facet_index={facet_index})"
),
}
.into());
}
for omit in 0..facet_vertices.len() {
ridge_vertices.clear();
for (j, &vk) in facet_vertices.iter().enumerate() {
if j == omit {
continue;
}
ridge_vertices.push(vk);
}
let ridge_key = facet_key_from_vertices(&ridge_vertices);
*ridge_to_boundary_facet_count.entry(ridge_key).or_insert(0) += 1;
}
}
for (ridge_key, boundary_facet_count) in ridge_to_boundary_facet_count {
if boundary_facet_count != 2 {
return Err(ManifoldError::BoundaryRidgeMultiplicity {
ridge_key,
boundary_facet_count,
});
}
}
Ok(())
}
pub(crate) fn validate_local_pseudomanifold_for_simplices<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
simplices: &[SimplexKey],
) -> Result<(), ManifoldError> {
if D == 0 || simplices.is_empty() {
return Ok(());
}
let facet_to_simplices = build_local_facet_star_map(tds, simplices)?;
validate_facet_degree(&facet_to_simplices)?;
validate_closed_boundary_for_local_facets(tds, &facet_to_simplices)
}
fn build_local_facet_star_map<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
simplices: &[SimplexKey],
) -> Result<FacetToSimplicesMap, ManifoldError> {
let mut facet_to_simplices = FacetToSimplicesMap::default();
let mut seen_facets: FastHashSet<u64> = FastHashSet::default();
for &simplex_key in simplices {
let simplex_vertices = tds.simplex_vertices(simplex_key)?;
for facet_index in 0..simplex_vertices.len() {
let (facet_vertices, facet_vertices_bare) =
simplex_facet_vertex_ids(tds, simplex_key, facet_index)?;
let facet_key = periodic_simplex_key(&facet_vertices);
if !seen_facets.insert(facet_key) {
continue;
}
let incident = facet_incident_handles(tds, facet_key, &facet_vertices_bare)?;
facet_to_simplices.insert(facet_key, incident);
}
}
Ok(facet_to_simplices)
}
fn simplex_facet_vertex_ids<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
simplex_key: SimplexKey,
facet_index: usize,
) -> Result<(LiftedVertexBuffer, VertexKeyBuffer), ManifoldError> {
let simplex_vertices = tds.simplex_vertices(simplex_key)?;
if facet_index >= simplex_vertices.len() {
return Err(TdsError::IndexOutOfBounds {
index: facet_index,
bound: simplex_vertices.len(),
context: "local lifted facet vertex extraction".to_string(),
}
.into());
}
let offsets = tds
.simplex(simplex_key)
.and_then(|simplex| simplex.periodic_vertex_offsets());
let mut lifted_vertices =
LiftedVertexBuffer::with_capacity(simplex_vertices.len().saturating_sub(1));
let mut bare_vertices =
VertexKeyBuffer::with_capacity(simplex_vertices.len().saturating_sub(1));
for (idx, &vertex_key) in simplex_vertices.iter().enumerate() {
if idx == facet_index {
continue;
}
bare_vertices.push(vertex_key);
let lifted = offsets.map_or_else(
|| LiftedVertexId::base(vertex_key),
|simplex_offsets| lifted_vertex_id(vertex_key, &simplex_offsets[idx]),
);
lifted_vertices.push(lifted);
}
Ok((lifted_vertices, bare_vertices))
}
fn facet_incident_handles<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
facet_key: u64,
facet_vertices_bare: &[VertexKey],
) -> Result<SmallBuffer<FacetHandle, 2>, ManifoldError> {
let candidate_simplices = simplex_star_simplices(tds, facet_vertices_bare)?;
let mut handles: SmallBuffer<FacetHandle, 2> =
SmallBuffer::with_capacity(candidate_simplices.len().max(1));
for simplex_key in candidate_simplices {
let simplex_vertices = tds.simplex_vertices(simplex_key)?;
for candidate_facet_index in 0..simplex_vertices.len() {
let (candidate_vertices, _candidate_vertices_bare) =
simplex_facet_vertex_ids(tds, simplex_key, candidate_facet_index)?;
if periodic_simplex_key(&candidate_vertices) != facet_key {
continue;
}
let Ok(facet_index) = u8::try_from(candidate_facet_index) else {
return Err(TdsError::IndexOutOfBounds {
index: candidate_facet_index,
bound: u8::MAX as usize + 1,
context: "local facet incident handle".to_string(),
}
.into());
};
handles.push(FacetHandle::new(simplex_key, facet_index));
}
}
Ok(handles)
}
fn validate_closed_boundary_for_local_facets<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
facet_to_simplices: &FacetToSimplicesMap,
) -> Result<(), ManifoldError> {
if D < 2 {
return Ok(());
}
let mut checked_ridges: FastHashSet<u64> = FastHashSet::default();
for simplex_facet_pairs in facet_to_simplices.values() {
let [handle] = simplex_facet_pairs.as_slice() else {
continue;
};
if !is_boundary_facet_handle(tds, *handle)? {
continue;
}
let (facet_vertices, facet_vertices_bare) =
simplex_facet_vertex_ids(tds, handle.simplex_key(), handle.facet_index() as usize)?;
for (ridge_vertices, ridge_vertices_bare) in
ridge_vertices_for_facet::<D>(&facet_vertices, &facet_vertices_bare)?
{
let ridge_key = periodic_simplex_key(&ridge_vertices);
if !checked_ridges.insert(ridge_key) {
continue;
}
let boundary_facet_count =
boundary_facet_count_for_ridge(tds, &ridge_vertices, &ridge_vertices_bare)?;
if boundary_facet_count != 2 {
return Err(ManifoldError::BoundaryRidgeMultiplicity {
ridge_key,
boundary_facet_count,
});
}
}
}
Ok(())
}
fn ridge_vertices_for_facet<const D: usize>(
facet_vertices: &[LiftedVertexId],
facet_vertices_bare: &[VertexKey],
) -> Result<SmallBuffer<(LiftedVertexBuffer, VertexKeyBuffer), 8>, ManifoldError> {
if facet_vertices.len() != D {
return Err(TdsError::DimensionMismatch {
expected: D,
actual: facet_vertices.len(),
context: "local boundary facet vertex count".to_string(),
}
.into());
}
if facet_vertices_bare.len() != D {
return Err(TdsError::DimensionMismatch {
expected: D,
actual: facet_vertices_bare.len(),
context: "local boundary facet bare vertex count".to_string(),
}
.into());
}
let mut ridges: SmallBuffer<(LiftedVertexBuffer, VertexKeyBuffer), 8> =
SmallBuffer::with_capacity(D);
for omit in 0..facet_vertices.len() {
let mut ridge_vertices = LiftedVertexBuffer::with_capacity(D.saturating_sub(1));
let mut ridge_vertices_bare = VertexKeyBuffer::with_capacity(D.saturating_sub(1));
for (idx, vertex_key) in facet_vertices.iter().enumerate() {
if idx != omit {
ridge_vertices.push(vertex_key.clone());
ridge_vertices_bare.push(facet_vertices_bare[idx]);
}
}
ridges.push((ridge_vertices, ridge_vertices_bare));
}
Ok(ridges)
}
fn boundary_facet_count_for_ridge<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
ridge_vertices: &[LiftedVertexId],
ridge_vertices_bare: &[VertexKey],
) -> Result<usize, ManifoldError> {
let star_simplices = simplex_star_simplices(tds, ridge_vertices_bare)?;
let mut count = 0usize;
let mut seen_boundary_facets: FastHashSet<u64> = FastHashSet::default();
for simplex_key in star_simplices {
let simplex_vertices = tds.simplex_vertices(simplex_key)?;
for facet_index in 0..simplex_vertices.len() {
let (facet_vertices, facet_vertices_bare) =
simplex_facet_vertex_ids(tds, simplex_key, facet_index)?;
if !ridge_vertices
.iter()
.all(|ridge_vertex| facet_vertices.contains(ridge_vertex))
{
continue;
}
let facet_key = periodic_simplex_key(&facet_vertices);
if !seen_boundary_facets.insert(facet_key) {
continue;
}
let handles = facet_incident_handles(tds, facet_key, &facet_vertices_bare)?;
match handles.len() {
1 => {
if is_boundary_facet_handle(tds, handles[0])? {
count = count.saturating_add(1);
}
}
2 => {}
other => {
return Err(ManifoldError::ManifoldFacetMultiplicity {
facet_key,
simplex_count: other,
});
}
}
}
}
Ok(count)
}
fn is_boundary_facet_handle<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
handle: FacetHandle,
) -> Result<bool, ManifoldError> {
let simplex_key = handle.simplex_key();
let facet_index = handle.facet_index() as usize;
let simplex = tds
.simplex(simplex_key)
.ok_or_else(|| TdsError::SimplexNotFound {
simplex_key,
context: "boundary facet classification".to_string(),
})?;
let is_periodic_self_identified = simplex
.neighbor_key(facet_index)
.is_some_and(|neighbor| neighbor == Some(simplex_key))
&& simplex.periodic_vertex_offsets().is_some_and(|offsets| {
!offsets.is_empty() && offsets.len() == simplex.number_of_vertices()
});
Ok(!is_periodic_self_identified)
}
fn simplex_star_simplices<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
simplex_vertices: &[VertexKey],
) -> Result<SmallBuffer<SimplexKey, 8>, ManifoldError> {
if simplex_vertices.is_empty() {
return Err(TdsError::InconsistentDataStructure {
message: "simplex_star_simplices requires at least one vertex".to_string(),
}
.into());
}
for &vk in simplex_vertices {
if !tds.contains_vertex_key(vk) {
return Err(TdsError::VertexNotFound {
vertex_key: vk,
context: "simplex star computation".to_string(),
}
.into());
}
}
let candidates: SimplexKeySet =
tds.find_simplices_containing_vertex_by_key(simplex_vertices[0]);
let mut star_simplices: SmallBuffer<SimplexKey, 8> =
SmallBuffer::with_capacity(candidates.len());
for simplex_key in candidates {
let candidate_vertices = tds.simplex_vertices(simplex_key)?;
if simplex_vertices
.iter()
.all(|&sv| candidate_vertices.contains(&sv))
{
star_simplices.push(simplex_key);
}
}
Ok(star_simplices)
}
fn simplex_link_simplices_from_star<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
simplex_vertices: &[VertexKey],
star_simplices: &[SimplexKey],
) -> Result<LinkSimplexBuffer, ManifoldError> {
if simplex_vertices.is_empty() {
return Err(TdsError::InconsistentDataStructure {
message: "simplex_link_simplices_from_star requires at least one vertex".to_string(),
}
.into());
}
let expected_link_vertices = (D + 1).saturating_sub(simplex_vertices.len());
let mut link_simplices: LinkSimplexBuffer = SmallBuffer::with_capacity(star_simplices.len());
let mut seen_link_simplices: FastHashSet<u64> =
fast_hash_set_with_capacity(star_simplices.len().max(1));
for &simplex_key in star_simplices {
let candidate_vertices = tds.simplex_vertices(simplex_key)?;
let offsets = tds
.simplex(simplex_key)
.and_then(|c| c.periodic_vertex_offsets());
let ref_slot = offsets.and_then(|_| {
candidate_vertices
.iter()
.position(|cv| simplex_vertices.contains(cv))
});
let mut link_vertices: LiftedVertexBuffer =
LiftedVertexBuffer::with_capacity(expected_link_vertices);
for (i, &vk) in candidate_vertices.iter().enumerate() {
if !simplex_vertices.contains(&vk) {
let lifted = match (offsets, ref_slot) {
(Some(offs), Some(r)) => {
let rel: SmallBuffer<i16, 8> = offs[i]
.iter()
.zip(offs[r].iter())
.map(|(&a, &b)| i16::from(a) - i16::from(b))
.collect();
lifted_vertex_id(vk, &rel)
}
_ => LiftedVertexId::base(vk),
};
link_vertices.push(lifted);
}
}
if link_vertices.len() != expected_link_vertices {
return Err(TdsError::DimensionMismatch {
expected: expected_link_vertices,
actual: link_vertices.len(),
context: format!(
"simplex link vertex count for {D}D (simplex_key={simplex_key:?})"
),
}
.into());
}
let link_key = anchored_lifted_simplex_key(&link_vertices);
if seen_link_simplices.insert(link_key) {
link_simplices.push(link_vertices);
}
}
Ok(link_simplices)
}
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "Not used yet; intended for reuse by future local topology mutations (e.g. bistellar flips)"
)
)]
pub(crate) fn ridge_star_simplices<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
ridge_vertices: &[VertexKey],
) -> Result<SmallBuffer<SimplexKey, 8>, ManifoldError> {
if D < 2 {
return Ok(SmallBuffer::new());
}
let expected_ridge_vertices = D.saturating_sub(1);
if ridge_vertices.len() != expected_ridge_vertices {
return Err(TdsError::DimensionMismatch {
expected: expected_ridge_vertices,
actual: ridge_vertices.len(),
context: format!("ridge vertex count for {D}D"),
}
.into());
}
simplex_star_simplices(tds, ridge_vertices)
}
fn ridge_link_edges_from_star<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
ridge_vertices: &[LiftedVertexId],
star_simplices: &[SimplexKey],
) -> Result<SmallBuffer<(LiftedVertexId, LiftedVertexId), 8>, ManifoldError> {
if D < 2 {
return Ok(SmallBuffer::new());
}
let expected_ridge_vertices = D.saturating_sub(1);
if ridge_vertices.len() != expected_ridge_vertices {
return Err(TdsError::DimensionMismatch {
expected: expected_ridge_vertices,
actual: ridge_vertices.len(),
context: format!("ridge vertex count for {D}D (link edges)"),
}
.into());
}
let mut link_edges: SmallBuffer<(LiftedVertexId, LiftedVertexId), 8> =
SmallBuffer::with_capacity(star_simplices.len());
let mut link_vertices: LiftedVertexBuffer = LiftedVertexBuffer::with_capacity(2);
for &simplex_key in star_simplices {
let Some(simplex_vertices) =
normalized_simplex_vertices_for_lifted_target(tds, simplex_key, ridge_vertices)?
else {
return Err(TdsError::InconsistentDataStructure {
message: format!(
"ridge star simplex {simplex_key:?} does not contain normalized ridge vertices \
{ridge_vertices:?}"
),
}
.into());
};
link_vertices.clear();
for lifted in simplex_vertices {
if !ridge_vertices.contains(&lifted) {
link_vertices.push(lifted);
}
}
if link_vertices.len() != 2 {
return Err(TdsError::DimensionMismatch {
expected: 2,
actual: link_vertices.len(),
context: format!("ridge link vertex count for {D}D (simplex_key={simplex_key:?})"),
}
.into());
}
if link_vertices[0] == link_vertices[1] {
return Err(TdsError::InconsistentDataStructure {
message: format!(
"Ridge link edge is a self-loop: link vertex {vk:?} repeated (simplex_key={simplex_key:?})",
vk = &link_vertices[0],
),
}
.into());
}
link_edges.push((link_vertices[0].clone(), link_vertices[1].clone()));
}
Ok(link_edges)
}
#[derive(Clone, Debug)]
struct RidgeStar {
ridge_vertices: LiftedVertexBuffer,
star_simplices: SmallBuffer<SimplexKey, 8>,
}
fn build_ridge_star_map<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
) -> Result<FastHashMap<u64, RidgeStar>, ManifoldError> {
let simplex_count = tds.number_of_simplices();
if simplex_count == 0 {
return Ok(FastHashMap::default());
}
let ridges_per_simplex = (D + 1).saturating_mul(D) / 2;
let estimated_unique_ridges = simplex_count
.saturating_mul(ridges_per_simplex)
.saturating_div(2)
.max(1);
let mut ridge_to_star: FastHashMap<u64, RidgeStar> =
fast_hash_map_with_capacity(estimated_unique_ridges);
let mut ridge_vertices: LiftedVertexBuffer =
LiftedVertexBuffer::with_capacity(D.saturating_sub(1));
for (simplex_key, simplex) in tds.simplices() {
let simplex_vertices = tds.simplex_vertices(simplex_key)?;
let offsets = simplex.periodic_vertex_offsets();
if simplex_vertices.len() != D + 1 {
return Err(TdsError::DimensionMismatch {
expected: D + 1,
actual: simplex_vertices.len(),
context: format!("simplex {simplex_key:?} vertex count for {D}D"),
}
.into());
}
for omit_a in 0..simplex_vertices.len() {
for omit_b in (omit_a + 1)..simplex_vertices.len() {
ridge_vertices.clear();
for (i, &vk) in simplex_vertices.iter().enumerate() {
if i == omit_a || i == omit_b {
continue;
}
let lifted = offsets.map_or_else(
|| LiftedVertexId::base(vk),
|offs| lifted_vertex_id(vk, &offs[i]),
);
ridge_vertices.push(lifted);
}
if ridge_vertices.len() != D.saturating_sub(1) {
return Err(TdsError::DimensionMismatch {
expected: D.saturating_sub(1),
actual: ridge_vertices.len(),
context: format!("ridge vertex count for {D}D (simplex_key={simplex_key:?}, omit_a={omit_a}, omit_b={omit_b})"),
}
.into());
}
let normalized_ridge_vertices = normalize_lifted_vertices(&ridge_vertices);
let ridge_key = periodic_simplex_key(&normalized_ridge_vertices);
let star = ridge_to_star.entry(ridge_key).or_insert_with(|| RidgeStar {
ridge_vertices: normalized_ridge_vertices.clone(),
star_simplices: SmallBuffer::new(),
});
star.star_simplices.push(simplex_key);
}
}
}
Ok(ridge_to_star)
}
fn build_ridge_star_map_for_simplices<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
simplices: &[SimplexKey],
) -> Result<FastHashMap<u64, RidgeStar>, ManifoldError> {
if D < 2 {
return Ok(FastHashMap::default());
}
if simplices.is_empty() {
return Ok(FastHashMap::default());
}
let ridges_per_simplex = (D + 1).saturating_mul(D) / 2;
let estimated_unique_ridges = simplices.len().saturating_mul(ridges_per_simplex).max(1);
let mut ridge_to_vertices: FastHashMap<u64, (LiftedVertexBuffer, VertexKeyBuffer)> =
fast_hash_map_with_capacity(estimated_unique_ridges);
let mut ridge_vertices_bare: VertexKeyBuffer =
VertexKeyBuffer::with_capacity(D.saturating_sub(1));
let mut ridge_vertices_lifted: LiftedVertexBuffer =
LiftedVertexBuffer::with_capacity(D.saturating_sub(1));
for &simplex_key in simplices {
if !tds.contains_simplex(simplex_key) {
continue;
}
let simplex_vertices = tds.simplex_vertices(simplex_key)?;
let offsets = tds
.simplex(simplex_key)
.and_then(|c| c.periodic_vertex_offsets());
if simplex_vertices.len() != D + 1 {
return Err(TdsError::DimensionMismatch {
expected: D + 1,
actual: simplex_vertices.len(),
context: format!("simplex {simplex_key:?} vertex count for {D}D (local ridge map)"),
}
.into());
}
for omit_a in 0..simplex_vertices.len() {
for omit_b in (omit_a + 1)..simplex_vertices.len() {
ridge_vertices_bare.clear();
ridge_vertices_lifted.clear();
for (i, &vk) in simplex_vertices.iter().enumerate() {
if i == omit_a || i == omit_b {
continue;
}
ridge_vertices_bare.push(vk);
let lifted = offsets.map_or_else(
|| LiftedVertexId::base(vk),
|offs| lifted_vertex_id(vk, &offs[i]),
);
ridge_vertices_lifted.push(lifted);
}
if ridge_vertices_bare.len() != D.saturating_sub(1) {
return Err(TdsError::DimensionMismatch {
expected: D.saturating_sub(1),
actual: ridge_vertices_bare.len(),
context: format!("ridge vertex count for {D}D (simplex_key={simplex_key:?}, omit_a={omit_a}, omit_b={omit_b})"),
}
.into());
}
let normalized_ridge_vertices = normalize_lifted_vertices(&ridge_vertices_lifted);
let ridge_key = periodic_simplex_key(&normalized_ridge_vertices);
ridge_to_vertices
.entry(ridge_key)
.or_insert_with(|| (normalized_ridge_vertices, ridge_vertices_bare.clone()));
}
}
}
let mut ridge_to_star: FastHashMap<u64, RidgeStar> =
fast_hash_map_with_capacity(ridge_to_vertices.len().max(1));
for (ridge_key, (lifted_vertices, bare_vertices)) in ridge_to_vertices {
let star_simplices =
periodic_aware_ridge_star(tds, ridge_key, &lifted_vertices, &bare_vertices)?;
ridge_to_star.insert(
ridge_key,
RidgeStar {
ridge_vertices: lifted_vertices,
star_simplices,
},
);
}
Ok(ridge_to_star)
}
fn normalized_simplex_vertices_for_lifted_target<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
simplex_key: SimplexKey,
target_vertices: &[LiftedVertexId],
) -> Result<Option<LiftedVertexBuffer>, ManifoldError> {
let simplex_vertices = tds.simplex_vertices(simplex_key)?;
let offsets = tds
.simplex(simplex_key)
.and_then(|simplex| simplex.periodic_vertex_offsets());
let Some(offsets) = offsets else {
let vertices: LiftedVertexBuffer = simplex_vertices
.iter()
.copied()
.map(LiftedVertexId::base)
.collect();
return Ok(Some(vertices));
};
let Some(anchor) = target_vertices.first() else {
return Ok(Some(LiftedVertexBuffer::new()));
};
let Some(anchor_index) = simplex_vertices
.iter()
.position(|&vertex_key| vertex_key == anchor.vertex_key)
else {
return Ok(None);
};
let anchor_offset = offsets[anchor_index];
let mut normalized = LiftedVertexBuffer::with_capacity(simplex_vertices.len());
for (idx, &vertex_key) in simplex_vertices.iter().enumerate() {
let mut relative_offset: SmallBuffer<i16, 8> = SmallBuffer::with_capacity(D);
for axis in 0..D {
relative_offset.push(i16::from(offsets[idx][axis]) - i16::from(anchor_offset[axis]));
}
normalized.push(lifted_vertex_id(vertex_key, &relative_offset));
}
Ok(Some(normalized))
}
fn periodic_aware_ridge_star<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
ridge_key: u64,
lifted_vertices: &[LiftedVertexId],
bare_vertices: &VertexKeyBuffer,
) -> Result<SmallBuffer<SimplexKey, 8>, ManifoldError> {
let all_star_simplices = simplex_star_simplices(tds, bare_vertices)?;
let mut star_simplices: SmallBuffer<SimplexKey, 8> =
SmallBuffer::with_capacity(all_star_simplices.len());
for &ck in &all_star_simplices {
let Some(normalized_vertices) =
normalized_simplex_vertices_for_lifted_target(tds, ck, lifted_vertices)?
else {
continue;
};
if lifted_vertices
.iter()
.all(|lv| normalized_vertices.contains(lv))
{
star_simplices.push(ck);
}
}
if star_simplices.is_empty() {
return Err(TdsError::InconsistentDataStructure {
message: format!(
"periodic offset filtering produced empty star for ridge \
{ridge_key:016x}: {count} candidate simplices were all excluded \
(lifted ridge vertices: {lifted:?})",
count = all_star_simplices.len(),
lifted = lifted_vertices,
),
}
.into());
}
Ok(star_simplices)
}
pub fn validate_ridge_links<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
) -> Result<(), ManifoldError> {
if D < 2 {
return Ok(());
}
if tds.number_of_simplices() == 0 {
return Ok(());
}
let ridge_to_star = build_ridge_star_map(tds)?;
for (ridge_key, star) in ridge_to_star {
let link_edges =
ridge_link_edges_from_star(tds, &star.ridge_vertices, &star.star_simplices)?;
if let Err(err) = validate_ridge_link_graph(ridge_key, &link_edges) {
#[cfg(debug_assertions)]
if std::env::var_os("DELAUNAY_DEBUG_RIDGE_LINK").is_some() {
let mut star_simplex_vertices: Vec<(SimplexKey, VertexKeyBuffer)> =
Vec::with_capacity(star.star_simplices.len());
for &simplex_key in &star.star_simplices {
match tds.simplex_vertices(simplex_key) {
Ok(vertices) => star_simplex_vertices.push((simplex_key, vertices)),
Err(_) => star_simplex_vertices.push((simplex_key, VertexKeyBuffer::new())),
}
}
tracing::warn!(
ridge_key = ridge_key,
ridge_vertices = ?star.ridge_vertices,
star_simplices = ?star.star_simplices,
star_simplex_vertices = ?star_simplex_vertices,
link_edges = ?link_edges,
"validate_ridge_links: ridge link validation failed"
);
}
return Err(err);
}
}
Ok(())
}
pub fn validate_ridge_links_for_simplices<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
simplices: &[SimplexKey],
) -> Result<(), ManifoldError> {
if D < 2 {
return Ok(());
}
if simplices.is_empty() {
return Ok(());
}
let ridge_to_star = build_ridge_star_map_for_simplices(tds, simplices)?;
for (ridge_key, star) in ridge_to_star {
let link_edges =
ridge_link_edges_from_star(tds, &star.ridge_vertices, &star.star_simplices)?;
if let Err(err) = validate_ridge_link_graph(ridge_key, &link_edges) {
#[cfg(debug_assertions)]
if std::env::var_os("DELAUNAY_DEBUG_RIDGE_LINK").is_some() {
let mut star_simplex_vertices: Vec<(SimplexKey, VertexKeyBuffer)> =
Vec::with_capacity(star.star_simplices.len());
for &simplex_key in &star.star_simplices {
match tds.simplex_vertices(simplex_key) {
Ok(vertices) => star_simplex_vertices.push((simplex_key, vertices)),
Err(_) => star_simplex_vertices.push((simplex_key, VertexKeyBuffer::new())),
}
}
tracing::warn!(
ridge_key = ridge_key,
ridge_vertices = ?star.ridge_vertices,
star_simplices = ?star.star_simplices,
star_simplex_vertices = ?star_simplex_vertices,
link_edges = ?link_edges,
"validate_ridge_links_for_simplices: ridge link validation failed"
);
}
return Err(err);
}
}
Ok(())
}
pub fn validate_vertex_links<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
facet_to_simplices: &FacetToSimplicesMap,
) -> Result<(), ManifoldError> {
if D < 1 {
return Ok(());
}
if tds.number_of_simplices() == 0 {
return Ok(());
}
let boundary_vertices = build_boundary_vertex_set(tds, facet_to_simplices)?;
for (vertex_key, _vertex) in tds.vertices() {
let interior_vertex = !boundary_vertices.contains(&vertex_key);
validate_single_vertex_link(tds, vertex_key, interior_vertex)?;
}
Ok(())
}
fn build_boundary_vertex_set<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
facet_to_simplices: &FacetToSimplicesMap,
) -> Result<FastHashSet<VertexKey>, ManifoldError> {
let mut boundary_vertices: FastHashSet<VertexKey> = FastHashSet::default();
for simplex_facet_pairs in facet_to_simplices.values() {
let [handle] = simplex_facet_pairs.as_slice() else {
continue;
};
let simplex_key = handle.simplex_key();
let facet_index = handle.facet_index() as usize;
let simplex_vertices = tds.simplex_vertices(simplex_key)?;
if facet_index >= simplex_vertices.len() {
return Err(TdsError::IndexOutOfBounds {
index: facet_index,
bound: simplex_vertices.len(),
context: format!("boundary facet index for simplex {simplex_key:?}"),
}
.into());
}
let mut facet_vertex_count = 0usize;
for (i, &vk) in simplex_vertices.iter().enumerate() {
if i == facet_index {
continue;
}
boundary_vertices.insert(vk);
facet_vertex_count += 1;
}
if facet_vertex_count != D {
return Err(TdsError::DimensionMismatch {
expected: D,
actual: facet_vertex_count,
context: format!(
"boundary facet vertex count (simplex_key={simplex_key:?}, facet_index={facet_index})"
),
}
.into());
}
}
Ok(boundary_vertices)
}
fn validate_vertex_link_d1(
vertex_key: VertexKey,
interior_vertex: bool,
link_simplices: &LinkSimplexBuffer,
) -> Result<(), ManifoldError> {
let mut link_vertices: FastHashSet<LiftedVertexId> =
fast_hash_set_with_capacity(link_simplices.len().max(1));
for simplex in link_simplices {
for vk in simplex {
link_vertices.insert(vk.clone());
}
}
let link_vertex_count = link_vertices.len();
let ok = if interior_vertex {
link_vertex_count == 2
} else {
link_vertex_count == 1
};
if ok {
Ok(())
} else {
Err(ManifoldError::VertexLinkNotManifold {
vertex_key,
link_vertex_count,
link_simplex_count: link_simplices.len(),
boundary_facet_count: usize::from(!interior_vertex),
max_degree: 0,
connected: true,
interior_vertex,
})
}
}
fn validate_vertex_link_d2(
vertex_key: VertexKey,
interior_vertex: bool,
link_simplices: &LinkSimplexBuffer,
link_vertex_count: usize,
link_simplex_count: usize,
) -> Result<(), ManifoldError> {
let boundary_facet_count = 0;
let Some((connected, max_degree, degree_one_vertices, _vertex_count)) =
link_1d_graph_stats(link_simplices)
else {
return Err(ManifoldError::VertexLinkNotManifold {
vertex_key,
link_vertex_count,
link_simplex_count,
boundary_facet_count,
max_degree: 0,
connected: false,
interior_vertex,
});
};
let expected_degree_one_vertices = if interior_vertex { 0 } else { 2 };
let ok_1d = connected && max_degree <= 2 && degree_one_vertices == expected_degree_one_vertices;
if ok_1d {
Ok(())
} else {
Err(ManifoldError::VertexLinkNotManifold {
vertex_key,
link_vertex_count,
link_simplex_count,
boundary_facet_count,
max_degree,
connected,
interior_vertex,
})
}
}
fn validate_single_vertex_link<T, U, V, const D: usize>(
tds: &Tds<T, U, V, D>,
vertex_key: VertexKey,
interior_vertex: bool,
) -> Result<(), ManifoldError> {
let star_simplices = simplex_star_simplices(tds, &[vertex_key])?;
if star_simplices.is_empty() {
return Err(ManifoldError::VertexLinkNotManifold {
vertex_key,
link_vertex_count: 0,
link_simplex_count: 0,
boundary_facet_count: 0,
max_degree: 0,
connected: true,
interior_vertex,
});
}
let link_simplices = simplex_link_simplices_from_star(tds, &[vertex_key], &star_simplices)?;
if D == 1 {
return validate_vertex_link_d1(vertex_key, interior_vertex, &link_simplices);
}
let mut link_vertex_set: FastHashSet<LiftedVertexId> =
fast_hash_set_with_capacity(link_simplices.len().saturating_mul(D).max(1));
for simplex in &link_simplices {
for vk in simplex {
link_vertex_set.insert(vk.clone());
}
}
let link_simplex_count = link_simplices.len();
let link_vertex_count = link_vertex_set.len();
if D == 2 {
return validate_vertex_link_d2(
vertex_key,
interior_vertex,
&link_simplices,
link_vertex_count,
link_simplex_count,
);
}
let (connected, max_degree) = link_1_skeleton_connectivity_and_max_degree(&link_simplices);
let (boundary_facet_count, link_is_manifold) =
validate_link_facets_and_boundary::<D>(&link_simplices, interior_vertex);
let link_topology_ok = if D == 3 {
let (chi, boundary_components) = if link_simplices_are_base(&link_simplices) {
let bare_simplices = bare_link_simplices(&link_simplices);
(
triangulated_surface_euler_characteristic(&bare_simplices),
triangulated_surface_boundary_component_count(&bare_simplices),
)
} else {
(
triangulated_surface_euler_characteristic_for_link(&link_simplices),
triangulated_surface_boundary_component_count_for_link(&link_simplices),
)
};
if interior_vertex {
chi == 2 && boundary_components == 0
} else {
chi == 1 && boundary_components == 1
}
} else {
true
};
let ok = connected && link_is_manifold && link_topology_ok;
if ok {
Ok(())
} else {
Err(ManifoldError::VertexLinkNotManifold {
vertex_key,
link_vertex_count,
link_simplex_count,
boundary_facet_count,
max_degree,
connected,
interior_vertex,
})
}
}
fn link_1_skeleton_connectivity_and_max_degree(
link_simplices: &LinkSimplexBuffer,
) -> (bool, usize) {
let mut unique_edges: FastHashSet<(LiftedVertexId, LiftedVertexId)> =
fast_hash_set_with_capacity(link_simplices.len().max(1));
let mut adjacency: FastHashMap<LiftedVertexId, LiftedVertexBuffer> =
fast_hash_map_with_capacity(link_simplices.len().saturating_mul(2).max(1));
for simplex in link_simplices {
for i in 0..simplex.len() {
for j in (i + 1)..simplex.len() {
let edge = ordered_lifted_edge(&simplex[i], &simplex[j]);
if !unique_edges.insert(edge.clone()) {
continue;
}
let (a, b) = edge;
adjacency.entry(a.clone()).or_default().push(b.clone());
adjacency.entry(b).or_default().push(a);
}
}
for vk in simplex {
adjacency.entry(vk.clone()).or_default();
}
}
let mut max_degree = 0usize;
for neighbors in adjacency.values() {
max_degree = max_degree.max(neighbors.len());
}
let connected = match adjacency.iter().next() {
None => true,
Some((start, _)) => {
let mut visited: FastHashSet<LiftedVertexId> =
fast_hash_set_with_capacity(adjacency.len().max(1));
let mut stack: LiftedVertexBuffer =
LiftedVertexBuffer::with_capacity(adjacency.len().max(1));
stack.push(start.clone());
while let Some(v) = stack.pop() {
if !visited.insert(v.clone()) {
continue;
}
let Some(neigh) = adjacency.get(&v) else {
continue;
};
for n in neigh {
if !visited.contains(n) {
stack.push(n.clone());
}
}
}
visited.len() == adjacency.len()
}
};
(connected, max_degree)
}
fn link_1d_graph_stats(link_simplices: &LinkSimplexBuffer) -> Option<(bool, usize, usize, usize)> {
let mut unique_edges: FastHashSet<(LiftedVertexId, LiftedVertexId)> =
fast_hash_set_with_capacity(link_simplices.len().max(1));
let mut adjacency: FastHashMap<LiftedVertexId, SmallBuffer<LiftedVertexId, 2>> =
fast_hash_map_with_capacity(link_simplices.len().saturating_mul(2).max(1));
for e in link_simplices {
if e.len() != 2 {
return None;
}
let edge = ordered_lifted_edge(&e[0], &e[1]);
if !unique_edges.insert(edge.clone()) {
continue;
}
let (a, b) = edge;
adjacency.entry(a.clone()).or_default().push(b.clone());
adjacency.entry(b).or_default().push(a);
}
let vertex_count = adjacency.len();
if vertex_count == 0 {
return None;
}
let mut max_degree = 0usize;
let mut degree_one_vertices = 0usize;
for neighbors in adjacency.values() {
max_degree = max_degree.max(neighbors.len());
if neighbors.len() == 1 {
degree_one_vertices += 1;
}
}
let connected = match adjacency.iter().next() {
None => true,
Some((start, _)) => {
let mut visited: FastHashSet<LiftedVertexId> =
fast_hash_set_with_capacity(vertex_count);
let mut stack: LiftedVertexBuffer = LiftedVertexBuffer::with_capacity(vertex_count);
stack.push(start.clone());
while let Some(v) = stack.pop() {
if !visited.insert(v.clone()) {
continue;
}
let Some(neigh) = adjacency.get(&v) else {
continue;
};
for n in neigh {
if !visited.contains(n) {
stack.push(n.clone());
}
}
}
visited.len() == vertex_count
}
};
Some((connected, max_degree, degree_one_vertices, vertex_count))
}
fn validate_link_facets_and_boundary<const D: usize>(
link_simplices: &LinkSimplexBuffer,
interior_vertex: bool,
) -> (usize, bool) {
#[derive(Clone, Debug)]
struct FacetInfo {
vertices: LiftedVertexBuffer,
count: usize,
}
let mut facet_map: FastHashMap<u64, FacetInfo> =
fast_hash_map_with_capacity(link_simplices.len().saturating_mul(D).max(1));
let mut facet_vertices: LiftedVertexBuffer =
LiftedVertexBuffer::with_capacity(D.saturating_sub(1));
for simplex in link_simplices {
if simplex.len() != D {
return (0, false);
}
for omit in 0..simplex.len() {
facet_vertices.clear();
for (j, vk) in simplex.iter().enumerate() {
if j == omit {
continue;
}
facet_vertices.push(vk.clone());
}
if facet_vertices.len() != D.saturating_sub(1) {
return (0, false);
}
let key = anchored_lifted_simplex_key(&facet_vertices);
let entry = facet_map.entry(key).or_insert_with(|| FacetInfo {
vertices: facet_vertices.clone(),
count: 0,
});
entry.count += 1;
}
}
let mut boundary_facet_count = 0usize;
for info in facet_map.values() {
match info.count {
1 => boundary_facet_count += 1,
2 => {}
_ => return (boundary_facet_count, false),
}
}
if interior_vertex && boundary_facet_count != 0 {
return (boundary_facet_count, false);
}
if boundary_facet_count > 0 {
let mut ridge_map: FastHashMap<u64, usize> = fast_hash_map_with_capacity(
boundary_facet_count
.saturating_mul(D.saturating_sub(1))
.max(1),
);
let mut ridge_vertices: LiftedVertexBuffer =
LiftedVertexBuffer::with_capacity(D.saturating_sub(2));
for info in facet_map.values() {
if info.count != 1 {
continue;
}
let f = &info.vertices;
for omit in 0..f.len() {
ridge_vertices.clear();
for (j, vk) in f.iter().enumerate() {
if j == omit {
continue;
}
ridge_vertices.push(vk.clone());
}
if ridge_vertices.len() != D.saturating_sub(2) {
return (boundary_facet_count, false);
}
let ridge_key = anchored_lifted_simplex_key(&ridge_vertices);
*ridge_map.entry(ridge_key).or_insert(0) += 1;
}
}
for c in ridge_map.values() {
if *c != 2 {
return (boundary_facet_count, false);
}
}
}
(boundary_facet_count, true)
}
fn link_simplices_are_base(link_simplices: &LinkSimplexBuffer) -> bool {
link_simplices
.iter()
.flat_map(|simplex| simplex.iter())
.all(LiftedVertexId::is_base)
}
fn bare_link_simplices(link_simplices: &LinkSimplexBuffer) -> SmallBuffer<VertexKeyBuffer, 8> {
link_simplices
.iter()
.map(|simplex| {
simplex
.iter()
.map(|vertex| vertex.vertex_key)
.collect::<VertexKeyBuffer>()
})
.collect()
}
fn triangulated_surface_euler_characteristic_for_link(link_simplices: &LinkSimplexBuffer) -> isize {
let mut vertices: FastHashSet<LiftedVertexId> =
fast_hash_set_with_capacity(link_simplices.len().saturating_mul(3).max(1));
let mut edges: FastHashSet<(LiftedVertexId, LiftedVertexId)> =
fast_hash_set_with_capacity(link_simplices.len().saturating_mul(3).max(1));
for simplex in link_simplices {
for vertex in simplex {
vertices.insert(vertex.clone());
}
for i in 0..simplex.len() {
for j in (i + 1)..simplex.len() {
edges.insert(ordered_lifted_edge(&simplex[i], &simplex[j]));
}
}
}
vertices.len().cast_signed() - edges.len().cast_signed() + link_simplices.len().cast_signed()
}
fn triangulated_surface_boundary_component_count_for_link(
link_simplices: &LinkSimplexBuffer,
) -> usize {
let mut edge_counts: FastHashMap<(LiftedVertexId, LiftedVertexId), usize> =
fast_hash_map_with_capacity(link_simplices.len().saturating_mul(3).max(1));
for simplex in link_simplices {
if simplex.len() < 2 {
continue;
}
for i in 0..simplex.len() {
for j in (i + 1)..simplex.len() {
*edge_counts
.entry(ordered_lifted_edge(&simplex[i], &simplex[j]))
.or_insert(0) += 1;
}
}
}
let boundary_edges: SmallBuffer<(LiftedVertexId, LiftedVertexId), 8> = edge_counts
.into_iter()
.filter_map(|(edge, count)| (count == 1).then_some(edge))
.collect();
if boundary_edges.is_empty() {
return 0;
}
let mut adjacency: FastHashMap<LiftedVertexId, LiftedVertexBuffer> =
fast_hash_map_with_capacity(boundary_edges.len().saturating_mul(2));
for (a, b) in boundary_edges {
adjacency.entry(a.clone()).or_default().push(b.clone());
adjacency.entry(b).or_default().push(a);
}
let mut visited: FastHashSet<LiftedVertexId> = fast_hash_set_with_capacity(adjacency.len());
let mut components = 0usize;
for start in adjacency.keys() {
if visited.contains(start) {
continue;
}
components += 1;
let mut stack: LiftedVertexBuffer = LiftedVertexBuffer::with_capacity(adjacency.len());
stack.push(start.clone());
while let Some(vertex) = stack.pop() {
if !visited.insert(vertex.clone()) {
continue;
}
let Some(neighbors) = adjacency.get(&vertex) else {
continue;
};
for neighbor in neighbors {
if !visited.contains(neighbor) {
stack.push(neighbor.clone());
}
}
}
}
components
}
fn validate_ridge_link_graph(
ridge_key: u64,
link_edges: &[(LiftedVertexId, LiftedVertexId)],
) -> Result<(), ManifoldError> {
let mut unique_edges: FastHashSet<(LiftedVertexId, LiftedVertexId)> =
fast_hash_set_with_capacity(link_edges.len().max(1));
let estimated_link_vertices = link_edges.len().saturating_mul(2).max(1);
let mut adjacency: FastHashMap<LiftedVertexId, SmallBuffer<LiftedVertexId, 2>> =
fast_hash_map_with_capacity(estimated_link_vertices);
let mut max_degree = 0usize;
let mut link_edge_count = 0usize;
for (a, b) in link_edges {
let edge = ordered_lifted_edge(a, b);
if !unique_edges.insert(edge.clone()) {
continue;
}
link_edge_count += 1;
let (a, b) = edge;
let a_neighbors = adjacency.entry(a.clone()).or_default();
a_neighbors.push(b.clone());
max_degree = max_degree.max(a_neighbors.len());
let b_neighbors = adjacency.entry(b).or_default();
b_neighbors.push(a);
max_degree = max_degree.max(b_neighbors.len());
}
let link_vertex_count = adjacency.len();
let degree_one_vertices = adjacency.values().filter(|n| n.len() == 1).count();
let connected = match adjacency.iter().next() {
None => true,
Some((start, _)) => {
let mut visited: FastHashSet<LiftedVertexId> =
fast_hash_set_with_capacity(link_vertex_count);
let mut stack: LiftedVertexBuffer =
LiftedVertexBuffer::with_capacity(link_vertex_count);
stack.push(start.clone());
while let Some(v) = stack.pop() {
if !visited.insert(v.clone()) {
continue;
}
let Some(neighbors) = adjacency.get(&v) else {
continue;
};
for n in neighbors {
if !visited.contains(n) {
stack.push(n.clone());
}
}
}
visited.len() == link_vertex_count
}
};
let ok = connected && max_degree <= 2 && (degree_one_vertices == 0 || degree_one_vertices == 2);
if ok {
Ok(())
} else {
Err(ManifoldError::RidgeLinkNotManifold {
ridge_key,
link_vertex_count,
link_edge_count,
max_degree,
degree_one_vertices,
connected,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::facet::FacetHandle;
use crate::core::simplex::Simplex;
use crate::core::triangulation::Triangulation;
use crate::geometry::kernel::FastKernel;
use crate::vertex;
use slotmap::KeyData;
fn vk(id: u64) -> VertexKey {
VertexKey::from(KeyData::from_ffi(id))
}
fn simplex(vertices: &[VertexKey]) -> LiftedVertexBuffer {
let mut s: LiftedVertexBuffer = LiftedVertexBuffer::with_capacity(vertices.len());
s.extend(vertices.iter().copied().map(LiftedVertexId::base));
s
}
fn build_closed_surface_s2_tds_2d() -> (Tds<f64, (), (), 2>, [VertexKey; 4]) {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap();
for tri in [[v0, v1, v2], [v0, v1, v3], [v0, v2, v3], [v1, v2, v3]] {
tds.insert_simplex_with_mapping(
Simplex::new(vec![tri[0], tri[1], tri[2]], None).unwrap(),
)
.unwrap();
}
(tds, [v0, v1, v2, v3])
}
fn build_non_manifold_boundary_ridge_tds_3d() -> (Tds<f64, (), (), 3>, SimplexKey, u64) {
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let shared_edge_v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]))
.unwrap();
let shared_edge_v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]))
.unwrap();
let tet1_v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]))
.unwrap();
let tet1_v3 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0]))
.unwrap();
let tet2_v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, -1.0, 0.0]))
.unwrap();
let tet2_v3 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, -1.0]))
.unwrap();
let touched_simplex = tds
.insert_simplex_with_mapping(
Simplex::new(vec![shared_edge_v0, shared_edge_v1, tet1_v2, tet1_v3], None).unwrap(),
)
.unwrap();
let _ = tds
.insert_simplex_with_mapping(
Simplex::new(vec![shared_edge_v0, shared_edge_v1, tet2_v2, tet2_v3], None).unwrap(),
)
.unwrap();
let expected_ridge_key = facet_key_from_vertices(&[shared_edge_v0, shared_edge_v1]);
(tds, touched_simplex, expected_ridge_key)
}
#[test]
fn test_validate_facet_degree_ok_for_single_tetrahedron() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let tds =
Triangulation::<FastKernel<f64>, (), (), 3>::build_initial_simplex(&vertices).unwrap();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
assert!(validate_facet_degree(&facet_to_simplices).is_ok());
}
#[test]
fn test_validate_facet_degree_ok_for_two_tetrahedra_sharing_facet() {
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]))
.unwrap();
let v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]))
.unwrap();
let v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]))
.unwrap();
let v3 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0]))
.unwrap();
let v4 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, -1.0]))
.unwrap();
let _ = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap())
.unwrap();
let _ = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v4], None).unwrap())
.unwrap();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
assert!(validate_facet_degree(&facet_to_simplices).is_ok());
}
#[test]
fn test_validate_facet_degree_errors_on_non_manifold_facet_multiplicity() {
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]))
.unwrap();
let v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]))
.unwrap();
let v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]))
.unwrap();
let v3 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0]))
.unwrap();
let v4 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 2.0]))
.unwrap();
let v5 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 3.0]))
.unwrap();
let _ = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap())
.unwrap();
let _ = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v4], None).unwrap())
.unwrap();
let _ = tds
.insert_simplex_bypassing_topology_checks_for_test(
Simplex::new(vec![v0, v1, v2, v5], None).unwrap(),
)
.unwrap();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
let expected_facet_key = facet_key_from_vertices(&[v0, v1, v2]);
match validate_facet_degree(&facet_to_simplices) {
Err(ManifoldError::ManifoldFacetMultiplicity {
facet_key,
simplex_count,
}) => {
assert_eq!(facet_key, expected_facet_key);
assert_eq!(simplex_count, 3);
}
other => panic!("Expected ManifoldFacetMultiplicity, got {other:?}"),
}
}
#[test]
fn test_validate_closed_boundary_ok_for_single_tetrahedron() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let tds =
Triangulation::<FastKernel<f64>, (), (), 3>::build_initial_simplex(&vertices).unwrap();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
assert!(validate_closed_boundary(&tds, &facet_to_simplices).is_ok());
}
#[test]
fn test_validate_closed_boundary_errors_on_out_of_bounds_facet_index() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let tds =
Triangulation::<FastKernel<f64>, (), (), 3>::build_initial_simplex(&vertices).unwrap();
let simplex_key = tds.simplices().next().unwrap().0;
let mut facet_to_simplices: FacetToSimplicesMap = FacetToSimplicesMap::default();
let mut handles: SmallBuffer<FacetHandle, 2> = SmallBuffer::new();
handles.push(FacetHandle::new(simplex_key, u8::MAX));
facet_to_simplices.insert(0_u64, handles);
match validate_closed_boundary(&tds, &facet_to_simplices) {
Err(ManifoldError::Tds(TdsError::IndexOutOfBounds { index, bound, .. })) => {
assert!(
index >= bound,
"Expected index ({index}) >= bound ({bound})"
);
}
other => panic!("Expected IndexOutOfBounds error, got {other:?}"),
}
}
#[test]
fn test_validate_vertex_link_d1_accepts_interior_vertex_with_two_neighbors() {
let vertex_key = vk(0);
let mut link_simplices: LinkSimplexBuffer = SmallBuffer::new();
link_simplices.push(simplex(&[vk(1)]));
link_simplices.push(simplex(&[vk(2)]));
validate_vertex_link_d1(vertex_key, true, &link_simplices).unwrap();
}
#[test]
fn test_validate_vertex_link_d1_rejects_interior_vertex_with_one_neighbor() {
let vertex_key = vk(0);
let mut link_simplices: LinkSimplexBuffer = SmallBuffer::new();
link_simplices.push(simplex(&[vk(1)]));
match validate_vertex_link_d1(vertex_key, true, &link_simplices) {
Err(ManifoldError::VertexLinkNotManifold {
vertex_key: got,
link_vertex_count,
link_simplex_count,
boundary_facet_count,
interior_vertex,
..
}) => {
assert_eq!(got, vertex_key);
assert!(interior_vertex);
assert_eq!(link_vertex_count, 1);
assert_eq!(link_simplex_count, 1);
assert_eq!(boundary_facet_count, 0);
}
other => panic!("Expected VertexLinkNotManifold, got {other:?}"),
}
}
#[test]
fn test_validate_vertex_link_d1_accepts_boundary_vertex_with_one_neighbor() {
let vertex_key = vk(0);
let mut link_simplices: LinkSimplexBuffer = SmallBuffer::new();
link_simplices.push(simplex(&[vk(1)]));
validate_vertex_link_d1(vertex_key, false, &link_simplices).unwrap();
}
#[test]
fn test_validate_vertex_link_d1_rejects_boundary_vertex_with_two_neighbors() {
let vertex_key = vk(0);
let mut link_simplices: LinkSimplexBuffer = SmallBuffer::new();
link_simplices.push(simplex(&[vk(1)]));
link_simplices.push(simplex(&[vk(2)]));
match validate_vertex_link_d1(vertex_key, false, &link_simplices) {
Err(ManifoldError::VertexLinkNotManifold {
vertex_key: got,
link_vertex_count,
link_simplex_count,
boundary_facet_count,
interior_vertex,
..
}) => {
assert_eq!(got, vertex_key);
assert!(!interior_vertex);
assert_eq!(link_vertex_count, 2);
assert_eq!(link_simplex_count, 2);
assert_eq!(boundary_facet_count, 1);
}
other => panic!("Expected VertexLinkNotManifold, got {other:?}"),
}
}
#[test]
fn test_validate_vertex_link_d2_accepts_cycle_for_interior_vertex() {
let vertex_key = vk(0);
let a = vk(1);
let b = vk(2);
let c = vk(3);
let mut link_simplices: LinkSimplexBuffer = SmallBuffer::new();
link_simplices.push(simplex(&[a, b]));
link_simplices.push(simplex(&[b, c]));
link_simplices.push(simplex(&[c, a]));
validate_vertex_link_d2(vertex_key, true, &link_simplices, 3, 3).unwrap();
}
#[test]
fn test_validate_vertex_link_d2_accepts_path_for_boundary_vertex() {
let vertex_key = vk(0);
let a = vk(1);
let b = vk(2);
let c = vk(3);
let mut link_simplices: LinkSimplexBuffer = SmallBuffer::new();
link_simplices.push(simplex(&[a, b]));
link_simplices.push(simplex(&[b, c]));
validate_vertex_link_d2(vertex_key, false, &link_simplices, 3, 2).unwrap();
}
#[test]
fn test_validate_vertex_link_d2_rejects_path_for_interior_vertex() {
let vertex_key = vk(0);
let a = vk(1);
let b = vk(2);
let c = vk(3);
let mut link_simplices: LinkSimplexBuffer = SmallBuffer::new();
link_simplices.push(simplex(&[a, b]));
link_simplices.push(simplex(&[b, c]));
match validate_vertex_link_d2(vertex_key, true, &link_simplices, 3, 2) {
Err(ManifoldError::VertexLinkNotManifold {
vertex_key: got,
link_vertex_count,
link_simplex_count,
boundary_facet_count,
connected,
max_degree,
interior_vertex,
}) => {
assert_eq!(got, vertex_key);
assert!(interior_vertex);
assert_eq!(link_vertex_count, 3);
assert_eq!(link_simplex_count, 2);
assert_eq!(boundary_facet_count, 0);
assert!(connected);
assert_eq!(max_degree, 2);
}
other => panic!("Expected VertexLinkNotManifold, got {other:?}"),
}
}
#[test]
fn test_validate_vertex_link_d2_rejects_cycle_for_boundary_vertex() {
let vertex_key = vk(0);
let a = vk(1);
let b = vk(2);
let c = vk(3);
let mut link_simplices: LinkSimplexBuffer = SmallBuffer::new();
link_simplices.push(simplex(&[a, b]));
link_simplices.push(simplex(&[b, c]));
link_simplices.push(simplex(&[c, a]));
assert!(matches!(
validate_vertex_link_d2(vertex_key, false, &link_simplices, 3, 3),
Err(ManifoldError::VertexLinkNotManifold { .. })
));
}
#[test]
fn test_validate_vertex_link_d2_rejects_non_edge_link_simplices() {
let vertex_key = vk(0);
let mut link_simplices: LinkSimplexBuffer = SmallBuffer::new();
link_simplices.push(simplex(&[vk(1)]));
assert!(matches!(
validate_vertex_link_d2(vertex_key, true, &link_simplices, 1, 1),
Err(ManifoldError::VertexLinkNotManifold { .. })
));
}
#[test]
fn test_validate_single_vertex_link_d1_accepts_path_middle_and_endpoints() {
let mut tds: Tds<f64, (), (), 1> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([2.0])).unwrap();
tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v1], None).unwrap())
.unwrap();
tds.insert_simplex_with_mapping(Simplex::new(vec![v1, v2], None).unwrap())
.unwrap();
validate_single_vertex_link(&tds, v0, false).unwrap();
validate_single_vertex_link(&tds, v1, true).unwrap();
validate_single_vertex_link(&tds, v2, false).unwrap();
}
#[test]
fn test_validate_single_vertex_link_d1_rejects_endpoint_classified_as_interior() {
let mut tds: Tds<f64, (), (), 1> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0])).unwrap();
tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v1], None).unwrap())
.unwrap();
assert!(matches!(
validate_single_vertex_link(&tds, v0, true),
Err(ManifoldError::VertexLinkNotManifold { .. })
));
}
#[test]
fn test_validate_single_vertex_link_d2_accepts_boundary_vertex_in_single_triangle() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
tds.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
validate_single_vertex_link(&tds, v0, false).unwrap();
assert!(matches!(
validate_single_vertex_link(&tds, v0, true),
Err(ManifoldError::VertexLinkNotManifold { .. })
));
}
#[test]
fn test_validate_single_vertex_link_d2_accepts_interior_vertex_in_closed_surface() {
let (tds, [v0, ..]) = build_closed_surface_s2_tds_2d();
validate_single_vertex_link(&tds, v0, true).unwrap();
assert!(matches!(
validate_single_vertex_link(&tds, v0, false),
Err(ManifoldError::VertexLinkNotManifold { .. })
));
}
#[test]
fn test_validate_vertex_links_in_2d_disk_distinguishes_boundary_and_interior_vertices() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let va = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let vb = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let vc = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap();
let vd = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let center = tds.insert_vertex_with_mapping(vertex!([0.5, 0.5])).unwrap();
for tri in [
[center, va, vb],
[center, vb, vc],
[center, vc, vd],
[center, vd, va],
] {
tds.insert_simplex_with_mapping(
Simplex::new(vec![tri[0], tri[1], tri[2]], None).unwrap(),
)
.unwrap();
}
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
validate_facet_degree(&facet_to_simplices).unwrap();
validate_closed_boundary(&tds, &facet_to_simplices).unwrap();
let boundary_vertices = build_boundary_vertex_set(&tds, &facet_to_simplices).unwrap();
for boundary_vertex in [va, vb, vc, vd] {
assert!(boundary_vertices.contains(&boundary_vertex));
}
assert!(!boundary_vertices.contains(¢er));
let star_a = simplex_star_simplices(&tds, &[va]).unwrap();
let link_a = simplex_link_simplices_from_star(&tds, &[va], &star_a).unwrap();
let Some((connected, max_degree, degree_one_vertices, vertex_count)) =
link_1d_graph_stats(&link_a)
else {
panic!("Expected 1D link stats for boundary vertex");
};
assert!(connected);
assert_eq!(max_degree, 2);
assert_eq!(degree_one_vertices, 2);
assert_eq!(vertex_count, 3);
let star_o = simplex_star_simplices(&tds, &[center]).unwrap();
let link_o = simplex_link_simplices_from_star(&tds, &[center], &star_o).unwrap();
let Some((connected, max_degree, degree_one_vertices, vertex_count)) =
link_1d_graph_stats(&link_o)
else {
panic!("Expected 1D link stats for interior vertex");
};
assert!(connected);
assert_eq!(max_degree, 2);
assert_eq!(degree_one_vertices, 0);
assert_eq!(vertex_count, 4);
validate_vertex_links(&tds, &facet_to_simplices).unwrap();
assert!(matches!(
validate_single_vertex_link(&tds, va, true),
Err(ManifoldError::VertexLinkNotManifold { .. })
));
assert!(matches!(
validate_single_vertex_link(&tds, center, false),
Err(ManifoldError::VertexLinkNotManifold { .. })
));
}
#[test]
fn test_validate_closed_boundary_noop_for_d_lt_2() {
let vertices = vec![vertex!([0.0]), vertex!([1.0])];
let tds =
Triangulation::<FastKernel<f64>, (), (), 1>::build_initial_simplex(&vertices).unwrap();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
assert!(validate_closed_boundary(&tds, &facet_to_simplices).is_ok());
}
#[test]
fn test_validate_closed_boundary_noop_for_closed_2d_surface() {
let (tds, _vertices) = build_closed_surface_s2_tds_2d();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
assert!(
facet_to_simplices
.values()
.all(|handles| handles.len() == 2)
);
assert!(validate_closed_boundary(&tds, &facet_to_simplices).is_ok());
}
#[test]
fn test_validate_ridge_links_ok_for_closed_2d_surface() {
let (tds, _vertices) = build_closed_surface_s2_tds_2d();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
validate_facet_degree(&facet_to_simplices).unwrap();
validate_closed_boundary(&tds, &facet_to_simplices).unwrap();
assert!(validate_ridge_links(&tds).is_ok());
}
#[test]
fn test_simplex_star_simplices_errors_on_empty_simplex() {
let tds: Tds<f64, (), (), 2> = Tds::empty();
match simplex_star_simplices(&tds, &[]) {
Err(ManifoldError::Tds(TdsError::InconsistentDataStructure { ref message }))
if message.contains("at least one vertex") => {}
other => panic!("Expected InconsistentDataStructure for empty simplex, got {other:?}"),
}
}
#[test]
fn test_simplex_star_simplices_returns_empty_for_isolated_vertex() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let star = simplex_star_simplices(&tds, &[v0]).unwrap();
assert!(star.is_empty());
}
#[test]
fn test_simplex_link_simplices_from_star_errors_on_empty_simplex() {
let tds: Tds<f64, (), (), 2> = Tds::empty();
match simplex_link_simplices_from_star(&tds, &[], &[]) {
Err(ManifoldError::Tds(TdsError::InconsistentDataStructure { ref message }))
if message.contains("at least one vertex") => {}
other => panic!("Expected InconsistentDataStructure for empty simplex, got {other:?}"),
}
}
#[test]
fn test_simplex_link_simplices_from_star_errors_on_unrelated_vertex() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let v3 = tds
.insert_vertex_with_mapping(vertex!([10.0, 10.0]))
.unwrap();
let simplex_key = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
match simplex_link_simplices_from_star(&tds, &[v0, v3], &[simplex_key]) {
Err(ManifoldError::Tds(TdsError::DimensionMismatch {
expected: 1,
actual,
..
})) => {
assert_ne!(actual, 1, "Expected actual != 1 for unrelated vertex");
}
other => panic!("Expected DimensionMismatch for link-size mismatch, got {other:?}"),
}
}
#[test]
fn test_ridge_star_simplices_noop_for_d_lt_2() {
let tds: Tds<f64, (), (), 1> = Tds::empty();
let star = ridge_star_simplices(&tds, &[]).unwrap();
assert!(star.is_empty());
let star = ridge_star_simplices(&tds, &[VertexKey::from(KeyData::from_ffi(0))]).unwrap();
assert!(star.is_empty());
}
#[test]
fn test_ridge_link_edges_from_star_noop_for_d_lt_2() {
let tds: Tds<f64, (), (), 1> = Tds::empty();
let edges = ridge_link_edges_from_star(&tds, &[], &[]).unwrap();
assert!(edges.is_empty());
}
#[test]
fn test_ridge_star_simplices_errors_on_wrong_vertex_count_in_3d() {
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]))
.unwrap();
let v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]))
.unwrap();
let v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]))
.unwrap();
let v3 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0]))
.unwrap();
let _c = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap())
.unwrap();
match ridge_star_simplices(&tds, &[v0]) {
Err(ManifoldError::Tds(TdsError::DimensionMismatch {
expected: 2,
actual: 1,
..
})) => {}
other => panic!("Expected DimensionMismatch(2, 1) for wrong ridge size, got {other:?}"),
}
}
#[test]
fn test_ridge_link_edges_from_star_errors_on_wrong_vertex_count_in_3d() {
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]))
.unwrap();
let v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]))
.unwrap();
let v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]))
.unwrap();
let v3 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0]))
.unwrap();
let simplex_key = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap())
.unwrap();
match ridge_link_edges_from_star(&tds, &simplex(&[v0]), &[simplex_key]) {
Err(ManifoldError::Tds(TdsError::DimensionMismatch {
expected: 2,
actual: 1,
..
})) => {}
other => panic!("Expected DimensionMismatch(2, 1) for wrong ridge size, got {other:?}"),
}
}
#[test]
fn test_ridge_star_simplices_returns_incident_simplices_for_vertex_ridge_in_2d() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap();
let c012 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
let c013 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v3], None).unwrap())
.unwrap();
let c023 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v2, v3], None).unwrap())
.unwrap();
let _c123 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v1, v2, v3], None).unwrap())
.unwrap();
let star = ridge_star_simplices(&tds, &[v0]).unwrap();
let star_set: SimplexKeySet = star.iter().copied().collect();
let expected: SimplexKeySet = [c012, c013, c023].into_iter().collect();
assert_eq!(star_set, expected);
}
#[test]
fn test_ridge_star_simplices_errors_on_missing_vertex_key() {
let tds: Tds<f64, (), (), 2> = Tds::empty();
let missing = VertexKey::from(KeyData::from_ffi(u64::MAX));
match ridge_star_simplices(&tds, &[missing]) {
Err(ManifoldError::Tds(TdsError::VertexNotFound { vertex_key, .. })) => {
assert_eq!(vertex_key, missing);
}
other => panic!("Expected VertexNotFound error, got {other:?}"),
}
}
#[test]
fn test_ridge_link_edges_from_star_rejects_self_loop_edge() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let simplex_key = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
{
let simplex = tds
.simplex_mut(simplex_key)
.expect("simplex key should be valid in test");
simplex.clear_vertex_keys();
simplex.push_vertex_key(v0);
simplex.push_vertex_key(v1);
simplex.push_vertex_key(v1);
}
match ridge_link_edges_from_star(&tds, &simplex(&[v0]), &[simplex_key]) {
Err(ManifoldError::Tds(TdsError::InconsistentDataStructure { message })) => {
assert!(
message.contains("self-loop"),
"Unexpected message: {message}"
);
}
other => panic!("Expected self-loop edge error, got {other:?}"),
}
}
#[test]
fn test_validate_ridge_link_graph_deduplicates_parallel_edges() {
let a = VertexKey::from(KeyData::from_ffi(1));
let b = VertexKey::from(KeyData::from_ffi(2));
let c = VertexKey::from(KeyData::from_ffi(3));
let edges = vec![
(LiftedVertexId::base(a), LiftedVertexId::base(b)),
(LiftedVertexId::base(b), LiftedVertexId::base(c)),
(LiftedVertexId::base(c), LiftedVertexId::base(a)),
(LiftedVertexId::base(a), LiftedVertexId::base(b)),
];
assert!(validate_ridge_link_graph(0_u64, &edges).is_ok());
}
#[test]
fn test_validate_ridge_links_errors_on_corrupted_simplex_with_duplicate_vertices() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let simplex_key = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
{
let simplex = tds
.simplex_mut(simplex_key)
.expect("simplex key should be valid in test");
simplex.clear_vertex_keys();
simplex.push_vertex_key(v0);
simplex.push_vertex_key(v0);
simplex.push_vertex_key(v0);
}
match validate_ridge_links(&tds) {
Err(ManifoldError::Tds(TdsError::DimensionMismatch {
expected: 2,
actual,
..
})) => {
assert_ne!(actual, 2, "Expected actual != 2 for corrupted link");
}
other => {
panic!("Expected DimensionMismatch for ridge-link structural error, got {other:?}")
}
}
}
#[test]
fn test_validate_closed_boundary_errors_on_non_manifold_boundary_ridge() {
let (tds, _touched_simplex, expected_ridge_key) =
build_non_manifold_boundary_ridge_tds_3d();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
match validate_closed_boundary(&tds, &facet_to_simplices) {
Err(ManifoldError::BoundaryRidgeMultiplicity {
ridge_key,
boundary_facet_count,
}) => {
assert_eq!(ridge_key, expected_ridge_key);
assert_eq!(boundary_facet_count, 4);
}
other => panic!("Expected BoundaryRidgeMultiplicity, got {other:?}"),
}
}
#[test]
fn test_validate_local_pseudomanifold_for_simplices_errors_on_non_manifold_boundary_ridge() {
let (tds, touched_simplex, expected_ridge_key) = build_non_manifold_boundary_ridge_tds_3d();
match validate_local_pseudomanifold_for_simplices(&tds, &[touched_simplex]) {
Err(ManifoldError::BoundaryRidgeMultiplicity {
ridge_key,
boundary_facet_count,
}) => {
assert_eq!(ridge_key, expected_ridge_key);
assert_eq!(boundary_facet_count, 4);
}
other => panic!("Expected BoundaryRidgeMultiplicity, got {other:?}"),
}
}
#[test]
fn test_validate_local_pseudomanifold_for_simplices_errors_on_missing_scope_simplex() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let mut tds =
Triangulation::<FastKernel<f64>, (), (), 3>::build_initial_simplex(&vertices).unwrap();
let simplex_key = tds.simplex_keys().next().unwrap();
assert_eq!(tds.remove_simplices_by_keys(&[simplex_key]), 1);
match validate_local_pseudomanifold_for_simplices(&tds, &[simplex_key]) {
Err(ManifoldError::Tds(TdsError::SimplexNotFound {
simplex_key: missing_key,
..
})) => assert_eq!(missing_key, simplex_key),
other => panic!("Expected SimplexNotFound, got {other:?}"),
}
}
#[test]
fn test_validate_ridge_links_ok_for_single_tetrahedron() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let tds =
Triangulation::<FastKernel<f64>, (), (), 3>::build_initial_simplex(&vertices).unwrap();
assert!(validate_ridge_links(&tds).is_ok());
}
#[test]
fn test_validate_ridge_links_noop_for_empty_tds() {
let tds: Tds<f64, (), (), 2> = Tds::empty();
assert!(validate_ridge_links(&tds).is_ok());
}
#[test]
fn test_validate_ridge_links_rejects_wedge_at_vertex_in_2d() {
let (tds, v0, _incident, _nonincident) = build_wedge_two_spheres_share_vertex_tds_2d();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
validate_facet_degree(&facet_to_simplices).unwrap();
validate_closed_boundary(&tds, &facet_to_simplices).unwrap();
let expected_ridge_key = facet_key_from_vertices(&[v0]);
match validate_ridge_links(&tds) {
Err(ManifoldError::RidgeLinkNotManifold {
ridge_key,
connected,
degree_one_vertices,
max_degree,
..
}) => {
assert_eq!(ridge_key, expected_ridge_key);
assert!(!connected);
assert_eq!(degree_one_vertices, 0);
assert_eq!(max_degree, 2);
}
other => panic!("Expected RidgeLinkNotManifold, got {other:?}"),
}
}
fn build_two_tetrahedra_sharing_facet_tds_3d()
-> (Tds<f64, (), (), 3>, [VertexKey; 5], [SimplexKey; 2]) {
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]))
.unwrap();
let v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]))
.unwrap();
let v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]))
.unwrap();
let v3 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0]))
.unwrap();
let v4 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, -1.0]))
.unwrap();
let c1 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap())
.unwrap();
let c2 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v4], None).unwrap())
.unwrap();
(tds, [v0, v1, v2, v3, v4], [c1, c2])
}
fn build_wedge_two_spheres_share_vertex_tds_2d()
-> (Tds<f64, (), (), 2>, VertexKey, SimplexKey, SimplexKey) {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let v3 = tds.insert_vertex_with_mapping(vertex!([1.0, 1.0])).unwrap();
let c012 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
let _c013 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v3], None).unwrap())
.unwrap();
let _c023 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v2, v3], None).unwrap())
.unwrap();
let c123 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v1, v2, v3], None).unwrap())
.unwrap();
let v4 = tds
.insert_vertex_with_mapping(vertex!([10.0, 10.0]))
.unwrap();
let v5 = tds
.insert_vertex_with_mapping(vertex!([11.0, 10.0]))
.unwrap();
let v6 = tds
.insert_vertex_with_mapping(vertex!([10.0, 11.0]))
.unwrap();
let _c045 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v4, v5], None).unwrap())
.unwrap();
let _c046 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v4, v6], None).unwrap())
.unwrap();
let _c056 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v5, v6], None).unwrap())
.unwrap();
let _c456 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v4, v5, v6], None).unwrap())
.unwrap();
(tds, v0, c012, c123)
}
#[test]
fn test_build_ridge_star_map_empty_returns_empty() {
let tds: Tds<f64, (), (), 3> = Tds::empty();
let map = build_ridge_star_map(&tds).unwrap();
assert!(map.is_empty());
}
#[test]
fn test_build_ridge_star_map_errors_on_corrupted_simplex_vertex_count() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let simplex_key = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
{
let simplex = tds
.simplex_mut(simplex_key)
.expect("simplex key should be valid in test");
simplex.clear_vertex_keys();
simplex.push_vertex_key(v0);
simplex.push_vertex_key(v1);
}
match build_ridge_star_map(&tds) {
Err(ManifoldError::Tds(TdsError::DimensionMismatch {
expected: 3,
actual: 2,
..
})) => {}
other => {
panic!("Expected DimensionMismatch(3, 2) for corrupted simplex, got {other:?}")
}
}
}
#[test]
fn test_build_ridge_star_map_for_simplices_noop_for_d_lt_2() {
let tds: Tds<f64, (), (), 1> = Tds::empty();
let simplex_key = SimplexKey::from(KeyData::from_ffi(0));
let map = build_ridge_star_map_for_simplices(&tds, &[simplex_key]).unwrap();
assert!(map.is_empty());
}
#[test]
fn test_build_ridge_star_map_for_simplices_empty_returns_empty() {
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]))
.unwrap();
let v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]))
.unwrap();
let v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]))
.unwrap();
let v3 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0]))
.unwrap();
let _ = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap())
.unwrap();
let map = build_ridge_star_map_for_simplices(&tds, &[]).unwrap();
assert!(map.is_empty());
}
#[test]
fn test_build_ridge_star_map_for_simplices_3d_single_simplex_includes_only_its_ridges_and_full_stars()
{
let (tds, [v0, v1, v2, v3, v4], [c1, c2]) = build_two_tetrahedra_sharing_facet_tds_3d();
let missing = SimplexKey::from(KeyData::from_ffi(u64::MAX));
let map = build_ridge_star_map_for_simplices(&tds, &[c1, missing]).unwrap();
assert_eq!(map.len(), 6);
let star_set_for_edge = |a: VertexKey, b: VertexKey| -> SimplexKeySet {
let key = facet_key_from_vertices(&[a, b]);
let star = map
.get(&key)
.expect("expected ridge key in local ridge-star map");
assert_eq!(periodic_simplex_key(&star.ridge_vertices), key);
assert_eq!(star.ridge_vertices.len(), 2);
star.star_simplices.iter().copied().collect()
};
let shared_star: SimplexKeySet = [c1, c2].into_iter().collect();
let c1_only: SimplexKeySet = std::iter::once(c1).collect();
assert_eq!(star_set_for_edge(v0, v1), shared_star);
assert_eq!(star_set_for_edge(v0, v2), shared_star);
assert_eq!(star_set_for_edge(v1, v2), shared_star);
assert_eq!(star_set_for_edge(v0, v3), c1_only);
assert_eq!(star_set_for_edge(v1, v3), c1_only);
assert_eq!(star_set_for_edge(v2, v3), c1_only);
assert!(!map.contains_key(&facet_key_from_vertices(&[v0, v4])));
assert!(!map.contains_key(&facet_key_from_vertices(&[v1, v4])));
assert!(!map.contains_key(&facet_key_from_vertices(&[v2, v4])));
}
#[test]
fn test_build_ridge_star_map_for_simplices_3d_two_simplices_includes_union_of_ridges() {
let (tds, [v0, v1, v2, v3, v4], [c1, c2]) = build_two_tetrahedra_sharing_facet_tds_3d();
let map = build_ridge_star_map_for_simplices(&tds, &[c1, c2]).unwrap();
assert_eq!(map.len(), 9);
let star_size_for_edge = |a: VertexKey, b: VertexKey| -> usize {
let key = facet_key_from_vertices(&[a, b]);
map.get(&key)
.expect("expected ridge key in local ridge-star map")
.star_simplices
.len()
};
assert_eq!(star_size_for_edge(v0, v1), 2);
assert_eq!(star_size_for_edge(v0, v2), 2);
assert_eq!(star_size_for_edge(v1, v2), 2);
assert_eq!(star_size_for_edge(v0, v3), 1);
assert_eq!(star_size_for_edge(v1, v3), 1);
assert_eq!(star_size_for_edge(v2, v3), 1);
assert_eq!(star_size_for_edge(v0, v4), 1);
assert_eq!(star_size_for_edge(v1, v4), 1);
assert_eq!(star_size_for_edge(v2, v4), 1);
}
#[test]
fn test_build_ridge_star_map_for_simplices_2d_includes_full_star_for_shared_vertex() {
let (tds, v0, incident, _nonincident) = build_wedge_two_spheres_share_vertex_tds_2d();
let map = build_ridge_star_map_for_simplices(&tds, &[incident]).unwrap();
assert_eq!(map.len(), 3);
let ridge_key = facet_key_from_vertices(&[v0]);
let star = map
.get(&ridge_key)
.expect("expected ridge key for shared vertex");
assert_eq!(star.star_simplices.len(), 6);
}
#[test]
fn test_build_ridge_star_map_for_simplices_errors_on_corrupted_simplex_vertex_count() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.0, 1.0])).unwrap();
let simplex_key = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
{
let simplex = tds
.simplex_mut(simplex_key)
.expect("simplex key should be valid in test");
simplex.clear_vertex_keys();
simplex.push_vertex_key(v0);
simplex.push_vertex_key(v1);
}
match build_ridge_star_map_for_simplices(&tds, &[simplex_key]) {
Err(ManifoldError::Tds(TdsError::DimensionMismatch {
expected: 3,
actual: 2,
..
})) => {}
other => {
panic!("Expected DimensionMismatch(3, 2) for corrupted simplex, got {other:?}")
}
}
}
#[test]
fn test_validate_ridge_links_for_simplices_ok_for_single_tetrahedron_in_3d() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let tds =
Triangulation::<FastKernel<f64>, (), (), 3>::build_initial_simplex(&vertices).unwrap();
let simplices: Vec<SimplexKey> = tds.simplices().map(|(k, _)| k).collect();
validate_ridge_links_for_simplices(&tds, &simplices).unwrap();
validate_ridge_links_for_simplices(&tds, &[]).unwrap();
}
#[test]
fn test_validate_ridge_links_for_simplices_ok_for_missing_simplex_keys() {
let tds: Tds<f64, (), (), 3> = Tds::empty();
let missing = SimplexKey::from(KeyData::from_ffi(u64::MAX));
assert!(validate_ridge_links_for_simplices(&tds, &[missing]).is_ok());
}
#[test]
fn test_validate_ridge_links_for_simplices_noop_for_d_lt_2() {
let mut tds: Tds<f64, (), (), 1> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0])).unwrap();
let c01 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1], None).unwrap())
.unwrap();
assert!(validate_ridge_links_for_simplices(&tds, &[c01]).is_ok());
}
#[test]
fn test_validate_ridge_links_for_simplices_rejects_wedge_at_vertex_in_2d() {
let (tds, v0, incident, _nonincident) = build_wedge_two_spheres_share_vertex_tds_2d();
let expected_ridge_key = facet_key_from_vertices(&[v0]);
match validate_ridge_links_for_simplices(&tds, &[incident]) {
Err(ManifoldError::RidgeLinkNotManifold {
ridge_key,
link_vertex_count,
link_edge_count,
max_degree,
degree_one_vertices,
connected,
}) => {
assert_eq!(ridge_key, expected_ridge_key);
assert!(!connected);
assert_eq!(link_vertex_count, 6);
assert_eq!(link_edge_count, 6);
assert_eq!(max_degree, 2);
assert_eq!(degree_one_vertices, 0);
}
other => panic!("Expected RidgeLinkNotManifold, got {other:?}"),
}
}
#[test]
fn test_validate_ridge_links_for_simplices_only_checks_ridges_touched_by_input_simplices() {
let (tds, _v0, _incident, nonincident) = build_wedge_two_spheres_share_vertex_tds_2d();
assert!(validate_ridge_links_for_simplices(&tds, &[nonincident]).is_ok());
}
fn build_cone_on_torus_tds() -> (Tds<f64, (), (), 3>, VertexKey) {
const N: usize = 3;
const M: usize = 3;
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let mut v: [[VertexKey; M]; N] = [[VertexKey::from(KeyData::from_ffi(0)); M]; N];
for (i, row) in v.iter_mut().enumerate() {
for (j, slot) in row.iter_mut().enumerate() {
let i_f = <f64 as std::convert::From<u32>>::from(u32::try_from(i).unwrap());
let j_f = <f64 as std::convert::From<u32>>::from(u32::try_from(j).unwrap());
*slot = tds
.insert_vertex_with_mapping(vertex!([i_f, j_f, 0.0]))
.unwrap();
}
}
let apex = tds
.insert_vertex_with_mapping(vertex!([0.5, 0.5, 1.0]))
.unwrap();
for i in 0..N {
for j in 0..M {
let i1 = (i + 1) % N;
let j1 = (j + 1) % M;
let v00 = v[i][j];
let v10 = v[i1][j];
let v01 = v[i][j1];
let v11 = v[i1][j1];
for tri in [[v00, v10, v01], [v10, v11, v01]] {
let _ = tds
.insert_simplex_with_mapping(
Simplex::new(vec![tri[0], tri[1], tri[2], apex], None).unwrap(),
)
.unwrap();
}
}
}
(tds, apex)
}
#[test]
fn test_ridge_links_insufficient_for_pl_manifold() {
let (tds, apex) = build_cone_on_torus_tds();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
validate_facet_degree(&facet_to_simplices).unwrap();
validate_closed_boundary(&tds, &facet_to_simplices).unwrap();
assert!(validate_ridge_links(&tds).is_ok());
match validate_vertex_links(&tds, &facet_to_simplices) {
Err(ManifoldError::VertexLinkNotManifold {
vertex_key,
interior_vertex,
..
}) => {
assert_eq!(vertex_key, apex);
assert!(interior_vertex);
}
Ok(()) => panic!("Expected VertexLinkNotManifold for cone apex, got Ok(())"),
other => panic!("Expected VertexLinkNotManifold for cone apex, got {other:?}"),
}
}
#[test]
fn test_simplex_star_simplices_rejects_missing_vertex() {
let tds: Tds<f64, (), (), 2> = Tds::empty();
let stale_key = VertexKey::from(KeyData::from_ffi(0xDEAD));
match simplex_star_simplices(&tds, &[stale_key]) {
Err(ManifoldError::Tds(TdsError::VertexNotFound {
vertex_key,
ref context,
})) => {
assert_eq!(vertex_key, stale_key);
assert!(context.contains("simplex star"));
}
other => panic!("Expected VertexNotFound, got {other:?}"),
}
}
#[test]
fn test_ridge_star_simplices_rejects_too_many_vertices() {
let tds: Tds<f64, (), (), 3> = Tds::empty();
let v0 = VertexKey::from(KeyData::from_ffi(1));
let v1 = VertexKey::from(KeyData::from_ffi(2));
let v2 = VertexKey::from(KeyData::from_ffi(3));
match ridge_star_simplices(&tds, &[v0, v1, v2]) {
Err(ManifoldError::Tds(TdsError::DimensionMismatch {
expected, actual, ..
})) => {
assert_eq!(expected, 2);
assert_eq!(actual, 3);
}
other => panic!("Expected DimensionMismatch, got {other:?}"),
}
}
#[test]
fn test_validate_closed_boundary_dimension_mismatch_on_corrupted_simplex() {
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]))
.unwrap();
let v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]))
.unwrap();
let v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]))
.unwrap();
let v3 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0]))
.unwrap();
let simplex_key = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2, v3], None).unwrap())
.unwrap();
{
let simplex = tds.simplex_mut(simplex_key).unwrap();
simplex.clear_vertex_keys();
simplex.push_vertex_key(v0);
simplex.push_vertex_key(v1);
}
let mut facet_to_simplices: FacetToSimplicesMap = FacetToSimplicesMap::default();
let mut handles: SmallBuffer<FacetHandle, 2> = SmallBuffer::new();
handles.push(FacetHandle::new(simplex_key, 0));
facet_to_simplices.insert(0_u64, handles);
match validate_closed_boundary(&tds, &facet_to_simplices) {
Err(ManifoldError::Tds(TdsError::DimensionMismatch {
expected, actual, ..
})) => {
assert_eq!(expected, 3, "D=3: boundary facet should have 3 vertices");
assert!(
actual != 3,
"Corrupted simplex should produce wrong vertex count"
);
}
other => panic!("Expected DimensionMismatch, got {other:?}"),
}
}
#[test]
fn test_manifold_error_display_variants() {
let err = ManifoldError::ManifoldFacetMultiplicity {
facet_key: 0xABCD,
simplex_count: 3,
};
assert!(err.to_string().contains("Non-manifold facet"));
let err = ManifoldError::BoundaryRidgeMultiplicity {
ridge_key: 0x1234,
boundary_facet_count: 4,
};
assert!(err.to_string().contains("Boundary is not closed"));
let tds_err = TdsError::InconsistentDataStructure {
message: "inner".to_string(),
};
let err = ManifoldError::from(tds_err);
assert!(err.to_string().contains("inner"));
}
#[test]
fn test_validate_vertex_links_accepts_cone_on_sphere_in_3d() {
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]))
.unwrap();
let v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]))
.unwrap();
let v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]))
.unwrap();
let v3 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 1.0]))
.unwrap();
let apex = tds
.insert_vertex_with_mapping(vertex!([0.3, 0.3, 0.3]))
.unwrap();
let sphere_faces = vec![
vec![v0, v1, v2],
vec![v0, v1, v3],
vec![v0, v2, v3],
vec![v1, v2, v3],
];
for tri in sphere_faces {
let mut verts = tri;
verts.push(apex);
tds.insert_simplex_with_mapping(Simplex::new(verts, None).unwrap())
.unwrap();
}
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
validate_facet_degree(&facet_to_simplices).unwrap();
validate_closed_boundary(&tds, &facet_to_simplices).unwrap();
validate_ridge_links(&tds).unwrap();
validate_vertex_links(&tds, &facet_to_simplices).unwrap();
}
#[test]
fn test_build_ridge_star_map_for_simplices_identifies_translated_periodic_images() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.5, 1.0])).unwrap();
let mut simplex1 = Simplex::new(vec![v0, v1, v2], None).unwrap();
simplex1
.set_periodic_vertex_offsets(vec![[0, 0], [0, 0], [0, 0]])
.unwrap();
let c1 = tds.insert_simplex_with_mapping(simplex1).unwrap();
let mut simplex2 = Simplex::new(vec![v0, v1, v2], None).unwrap();
simplex2
.set_periodic_vertex_offsets(vec![[1, 0], [0, 0], [0, 0]])
.unwrap();
let c2 = tds.insert_simplex_with_mapping(simplex2).unwrap();
let map = build_ridge_star_map_for_simplices(&tds, &[c1, c2]).unwrap();
assert_eq!(map.len(), 3, "expected 3 quotient-aware ridges");
let shared_count = map.values().filter(|s| s.star_simplices.len() == 2).count();
assert_eq!(shared_count, 3, "three ridges should be shared");
}
#[test]
fn test_anchored_lifted_simplex_key_preserves_vertex_link_offsets() {
let mut tds: Tds<f64, (), (), 3> = Tds::empty();
let v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0, 0.0]))
.unwrap();
let v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0, 0.0]))
.unwrap();
let v2 = tds
.insert_vertex_with_mapping(vertex!([0.0, 1.0, 0.0]))
.unwrap();
let first_link_triangle: LiftedVertexBuffer = [
lifted_vertex_id(v0, &[1_i16, 0, 0]),
lifted_vertex_id(v1, &[1_i16, 0, 0]),
lifted_vertex_id(v2, &[1_i16, 0, 0]),
]
.into_iter()
.collect();
let shifted_link_triangle: LiftedVertexBuffer = [
lifted_vertex_id(v0, &[2_i16, 0, 0]),
lifted_vertex_id(v1, &[2_i16, 0, 0]),
lifted_vertex_id(v2, &[2_i16, 0, 0]),
]
.into_iter()
.collect();
assert_eq!(
periodic_simplex_key(&first_link_triangle),
periodic_simplex_key(&shifted_link_triangle),
"quotient simplex keys intentionally identify global translations"
);
assert_ne!(
anchored_lifted_simplex_key(&first_link_triangle),
anchored_lifted_simplex_key(&shifted_link_triangle),
"vertex-link keys must preserve offsets relative to the linked vertex"
);
}
#[test]
fn test_periodic_aware_ridge_star_empty_star_returns_error() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.5, 1.0])).unwrap();
let c1 = tds
.insert_simplex_with_mapping(Simplex::new(vec![v0, v1, v2], None).unwrap())
.unwrap();
tds.simplex_mut(c1)
.unwrap()
.set_periodic_vertex_offsets(vec![[0, 0], [0, 0], [0, 0]])
.unwrap();
let synthetic = lifted_vertex_id(v0, &[99_i16, 99_i16]);
let bare: VertexKeyBuffer = std::iter::once(v0).collect();
let lifted: LiftedVertexBuffer = std::iter::once(synthetic).collect();
match periodic_aware_ridge_star(&tds, 0x42, &lifted, &bare) {
Err(ManifoldError::Tds(TdsError::InconsistentDataStructure { ref message })) => {
assert!(
message.contains("empty star"),
"error should mention empty star: {message}"
);
}
other => panic!("Expected InconsistentDataStructure (empty star), got {other:?}"),
}
}
#[test]
fn test_validate_ridge_links_for_simplices_rejects_split_periodic_link() {
let mut tds: Tds<f64, (), (), 2> = Tds::empty();
let v0 = tds.insert_vertex_with_mapping(vertex!([0.0, 0.0])).unwrap();
let v1 = tds.insert_vertex_with_mapping(vertex!([1.0, 0.0])).unwrap();
let v2 = tds.insert_vertex_with_mapping(vertex!([0.5, 1.0])).unwrap();
let mut simplex1 = Simplex::new(vec![v0, v1, v2], None).unwrap();
simplex1
.set_periodic_vertex_offsets(vec![[0, 0], [0, 0], [0, 0]])
.unwrap();
let c1 = tds.insert_simplex_with_mapping(simplex1).unwrap();
let mut simplex2 = Simplex::new(vec![v0, v1, v2], None).unwrap();
simplex2
.set_periodic_vertex_offsets(vec![[1, 0], [0, 0], [0, 0]])
.unwrap();
let c2 = tds.insert_simplex_with_mapping(simplex2).unwrap();
match validate_ridge_links_for_simplices(&tds, &[c1, c2]) {
Err(ManifoldError::RidgeLinkNotManifold { .. }) => {}
other => panic!("Expected RidgeLinkNotManifold, got {other:?}"),
}
}
#[test]
fn test_validate_vertex_links_boundary_vertex_has_ball_link_in_3d() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let tds =
Triangulation::<FastKernel<f64>, (), (), 3>::build_initial_simplex(&vertices).unwrap();
let facet_to_simplices = tds.build_facet_to_simplices_map().unwrap();
validate_facet_degree(&facet_to_simplices).unwrap();
validate_closed_boundary(&tds, &facet_to_simplices).unwrap();
validate_vertex_links(&tds, &facet_to_simplices).unwrap();
}
}