#![forbid(unsafe_code)]
use std::fmt;
use thiserror::Error;
use crate::builder::DelaunayTriangulationBuilder;
use crate::collections::{FastHashMap, MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer};
use crate::construction::{
ConstructionOptions, DelaunayTriangulationConstructionError, InitialSimplexStrategy,
};
use crate::geometry::Point;
use crate::geometry::algorithms::convex_hull::{
ConvexHull, ConvexHullConstructionError, ConvexHullQueryError,
};
use crate::geometry::kernel::{AdaptiveKernel, ExactPredicates};
use crate::geometry::predicates::{Orientation, simplex_orientation};
use crate::geometry::traits::coordinate::{CoordinateConversionError, CoordinateValidationError};
use crate::geometry::util::safe_usize_to_scalar;
use crate::tds::{
Simplex, SimplexValidationError, Tds, TdsConstructionError, TdsError, TdsMutationError, Vertex,
};
use crate::topology::characteristics::validation::{
EulerClassificationEvidence, validate_euler_from_facets_with_evidence,
};
use crate::topology::manifold::{
HighDimensionalLinkEvidence, ManifoldError, ValidatedFacetDegreeMap,
validate_closed_boundary_from_validated_facet_map, validate_ridge_links,
validate_vertex_links_from_validated_facet_map_with_evidence,
};
use crate::topology::spaces::spherical::{
SphericalMetric, SphericalPoint, SphericalPointError, ambient_array_from_slice,
};
use crate::topology::traits::topological_space::{GlobalTopology, TopologyError};
use crate::validation_report::InvariantError;
use crate::{TopologyGuarantee, Triangulation, TriangulationValidationError, vertex};
const SPHERICAL_ROADMAP_ISSUE: u32 = 414;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SphericalSimplex<const D: usize> {
vertices: Vec<usize>,
}
impl<const D: usize> SphericalSimplex<D> {
pub fn try_new(
vertices: Vec<usize>,
vertex_count: usize,
) -> Result<Self, SphericalSimplexError> {
let expected = D
.checked_add(1)
.ok_or(SphericalSimplexError::UnrepresentableDimension { dimension: D })?;
if vertices.len() != expected {
return Err(SphericalSimplexError::InvalidArity {
dimension: D,
expected,
actual: vertices.len(),
});
}
for (position, &vertex_index) in vertices.iter().enumerate() {
if vertex_index >= vertex_count {
return Err(SphericalSimplexError::VertexIndexOutOfBounds {
vertex_index,
vertex_count,
});
}
for &other in &vertices[position + 1..] {
if vertex_index == other {
return Err(SphericalSimplexError::DuplicateVertex { vertex_index });
}
}
}
Ok(Self { vertices })
}
#[must_use]
pub fn vertex_indices(&self) -> &[usize] {
&self.vertices
}
#[must_use]
pub const fn dimension(&self) -> usize {
D
}
fn facet_key_omitting(&self, omitted_index: usize) -> Vec<usize> {
let mut facet = Vec::with_capacity(D);
for (index, &vertex) in self.vertices.iter().enumerate() {
if index != omitted_index {
facet.push(vertex);
}
}
facet.sort_unstable();
facet
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SphericalDelaunayTriangulation<const D: usize> {
points: Vec<SphericalPoint<D>>,
simplices: Vec<SphericalSimplex<D>>,
radius: f64,
construction_provenance: SphericalConstructionProvenance,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
enum SphericalConstructionProvenance {
#[default]
Unproven,
AmbientConvexHullBoundary,
}
impl SphericalConstructionProvenance {
const fn link_evidence(self) -> HighDimensionalLinkEvidence {
match self {
Self::Unproven => HighDimensionalLinkEvidence::Unproven,
Self::AmbientConvexHullBoundary => HighDimensionalLinkEvidence::PreservedByConstruction,
}
}
const fn euler_evidence(self) -> EulerClassificationEvidence {
match self {
Self::Unproven => EulerClassificationEvidence::Unproven,
Self::AmbientConvexHullBoundary => EulerClassificationEvidence::SphericalBoundary,
}
}
}
impl<const D: usize> SphericalDelaunayTriangulation<D> {
#[must_use]
pub fn points(&self) -> &[SphericalPoint<D>] {
&self.points
}
#[must_use]
pub fn simplices(&self) -> &[SphericalSimplex<D>] {
&self.simplices
}
#[must_use]
pub const fn radius(&self) -> f64 {
self.radius
}
#[must_use]
pub const fn dimension(&self) -> usize {
D
}
#[must_use]
pub const fn ambient_dimension(&self) -> usize {
D + 1
}
#[must_use]
pub const fn number_of_vertices(&self) -> usize {
self.points.len()
}
#[must_use]
pub const fn number_of_simplices(&self) -> usize {
self.simplices.len()
}
pub fn distance_between_vertices(
&self,
left: usize,
right: usize,
) -> Result<f64, SphericalDelaunayValidationError> {
let left_point = self.points.get(left).ok_or(
SphericalDelaunayValidationError::VertexIndexOutOfBounds {
vertex_index: left,
vertex_count: self.points.len(),
},
)?;
let right_point = self.points.get(right).ok_or(
SphericalDelaunayValidationError::VertexIndexOutOfBounds {
vertex_index: right,
vertex_count: self.points.len(),
},
)?;
SphericalMetric::<D>::try_new(self.radius)
.map_err(|source| SphericalDelaunayValidationError::Metric { source })?
.try_distance(left_point, right_point)
.map_err(
|source| SphericalDelaunayValidationError::GeodesicDistance {
left,
right,
source,
},
)
}
pub fn validate(&self) -> Result<(), SphericalDelaunayValidationError> {
self.validate_delaunay()
}
pub fn is_valid_topology(&self) -> Result<(), SphericalDelaunayValidationError> {
self.validate_topology()
}
pub fn is_valid_realization(&self) -> Result<(), SphericalDelaunayValidationError> {
self.validate_realization()
}
pub fn is_valid_delaunay(&self) -> Result<(), SphericalDelaunayValidationError> {
self.validate_delaunay()
}
pub fn validate_topology(&self) -> Result<(), SphericalDelaunayValidationError> {
SphericalMetric::<D>::try_new(self.radius)
.map_err(|source| SphericalDelaunayValidationError::Metric { source })?;
if self.simplices.is_empty() {
return Err(SphericalDelaunayValidationError::NoSimplices);
}
for (point_index, point) in self.points.iter().enumerate() {
if point.coords().len() != D + 1 {
return Err(
SphericalDelaunayValidationError::InvalidPointAmbientDimension {
point_index,
expected: D + 1,
actual: point.coords().len(),
},
);
}
let norm_over_radius = scaled_norm_over_radius(point.coords(), self.radius);
if (norm_over_radius - 1.0).abs() > 1.0e-12 {
return Err(SphericalDelaunayValidationError::PointNotOnSphere {
point_index,
radius: self.radius,
norm: scaled_euclidean_norm(point.coords()),
});
}
}
let mut simplex_indices: FastHashMap<Vec<usize>, usize> = FastHashMap::default();
let mut facet_counts: FastHashMap<Vec<usize>, usize> = FastHashMap::default();
let mut used_vertices = vec![false; self.points.len()];
for (simplex_index, simplex) in self.simplices.iter().enumerate() {
SphericalSimplex::<D>::try_new(simplex.vertex_indices().to_vec(), self.points.len())
.map_err(|source| SphericalDelaunayValidationError::InvalidSimplex {
simplex_index,
source,
})?;
let mut simplex_key = simplex.vertex_indices().to_vec();
simplex_key.sort_unstable();
if let Some(previous_simplex_index) =
simplex_indices.insert(simplex_key.clone(), simplex_index)
{
return Err(SphericalDelaunayValidationError::DuplicateSimplex {
simplex_index,
previous_simplex_index,
simplex_vertices: simplex_key,
});
}
for omitted_index in 0..=D {
let key = simplex.facet_key_omitting(omitted_index);
let count = facet_counts.entry(key).or_insert(0);
*count = count.saturating_add(1);
}
for &vertex_index in simplex.vertex_indices() {
used_vertices[vertex_index] = true;
}
}
for (facet_vertices, incident_count) in facet_counts {
if incident_count != 2 {
return Err(SphericalDelaunayValidationError::InvalidFacetIncidence {
facet_vertices,
incident_count,
});
}
}
for (vertex_index, is_used) in used_vertices.into_iter().enumerate() {
if !is_used {
return Err(SphericalDelaunayValidationError::UnusedVertex { vertex_index });
}
}
self.validate_intrinsic_pl_topology()?;
Ok(())
}
fn validate_intrinsic_pl_topology(&self) -> Result<(), SphericalDelaunayValidationError> {
let mut tds: Tds<(), (), D> = Tds::empty();
let mut vertex_keys = Vec::with_capacity(self.points.len());
for point_index in 0..self.points.len() {
let coords = synthetic_topology_coordinates::<D>(point_index).map_err(|source| {
SphericalDelaunayValidationError::AbstractTopologyVertex {
point_index,
source,
}
})?;
let vertex = vertex!(coords; data = ()).map_err(|source| {
SphericalDelaunayValidationError::AbstractTopologyVertex {
point_index,
source,
}
})?;
let vertex_key = tds.insert_vertex_with_mapping(vertex).map_err(|source| {
SphericalDelaunayValidationError::AbstractTopologyVertexInsertion {
point_index,
source: Box::new(source),
}
})?;
vertex_keys.push(vertex_key);
}
for (simplex_index, simplex) in self.simplices.iter().enumerate() {
let mut simplex_vertices = Vec::with_capacity(D + 1);
for &point_index in simplex.vertex_indices() {
let vertex_key = *vertex_keys.get(point_index).ok_or(
SphericalDelaunayValidationError::VertexIndexOutOfBounds {
vertex_index: point_index,
vertex_count: vertex_keys.len(),
},
)?;
simplex_vertices.push(vertex_key);
}
let abstract_simplex =
Simplex::try_new_with_data(simplex_vertices, None).map_err(|source| {
SphericalDelaunayValidationError::AbstractTopologySimplex {
simplex_index,
source,
}
})?;
tds.insert_simplex_with_mapping(abstract_simplex)
.map_err(|source| {
SphericalDelaunayValidationError::AbstractTopologySimplexInsertion {
simplex_index,
source: Box::new(source),
}
})?;
}
tds.assign_neighbors().map_err(intrinsic_tds_error)?;
tds.assign_incident_simplices().map_err(|source| {
SphericalDelaunayValidationError::AbstractTopologyMutation { source }
})?;
if !tds.is_connected() {
return Err(intrinsic_topology_error(
TriangulationValidationError::Disconnected {
simplex_count: tds.number_of_simplices(),
},
));
}
let facet_to_simplices = tds
.build_facet_to_simplices_map()
.map_err(intrinsic_tds_error)?;
let facet_to_simplices =
ValidatedFacetDegreeMap::try_from_facet_map(&tds, &facet_to_simplices)
.map_err(intrinsic_manifold_error)?;
validate_closed_boundary_from_validated_facet_map(
facet_to_simplices,
GlobalTopology::Spherical,
)
.map_err(intrinsic_manifold_error)?;
validate_ridge_links(&tds).map_err(intrinsic_manifold_error)?;
validate_vertex_links_from_validated_facet_map_with_evidence(
facet_to_simplices,
GlobalTopology::Spherical,
self.construction_provenance.link_evidence(),
)
.map_err(intrinsic_manifold_error)?;
let topology_result = validate_euler_from_facets_with_evidence(
facet_to_simplices,
GlobalTopology::Spherical,
self.construction_provenance.euler_evidence(),
)
.map_err(intrinsic_topology_support_error)?;
if let Some(expected) = topology_result.expected
&& topology_result.chi != expected
{
return Err(intrinsic_topology_error(
TriangulationValidationError::EulerCharacteristicMismatch {
computed: topology_result.chi,
expected,
classification: topology_result.classification,
},
));
}
Ok(())
}
pub fn validate_realization(&self) -> Result<(), SphericalDelaunayValidationError> {
self.validate_topology()?;
match D {
2 => self.validate_realization_with_ambient::<3>(),
3 => self.validate_realization_with_ambient::<4>(),
_ => Err(SphericalDelaunayValidationError::UnsupportedLayer {
layer: SphericalValidationLayer::Realization,
dimension: D,
tracking_issue: SPHERICAL_ROADMAP_ISSUE,
}),
}
}
pub fn validate_delaunay(&self) -> Result<(), SphericalDelaunayValidationError> {
self.validate_realization()?;
match D {
2 => self.validate_delaunay_with_ambient::<3>(),
3 => self.validate_delaunay_with_ambient::<4>(),
_ => Err(SphericalDelaunayValidationError::UnsupportedLayer {
layer: SphericalValidationLayer::Delaunay,
dimension: D,
tracking_issue: SPHERICAL_ROADMAP_ISSUE,
}),
}
}
fn validate_realization_with_ambient<const A: usize>(
&self,
) -> Result<(), SphericalDelaunayValidationError> {
Self::validate_ambient_dimension::<A>()?;
let origin = Self::ambient_origin::<A>()?;
for (simplex_index, simplex) in self.simplices.iter().enumerate() {
self.simplex_origin_side::<A>(simplex_index, simplex, origin)?;
}
Ok(())
}
fn validate_delaunay_with_ambient<const A: usize>(
&self,
) -> Result<(), SphericalDelaunayValidationError> {
Self::validate_ambient_dimension::<A>()?;
let origin = Self::ambient_origin::<A>()?;
for (simplex_index, simplex) in self.simplices.iter().enumerate() {
let origin_side = self.simplex_origin_side::<A>(simplex_index, simplex, origin)?;
for point_index in 0..self.points.len() {
if simplex.vertex_indices().contains(&point_index) {
continue;
}
let test_point = self.ambient_point::<A>(point_index)?;
let point_side = self.simplex_orientation_with_point::<A>(
simplex_index,
simplex,
test_point,
Some(point_index),
)?;
if point_side != Orientation::DEGENERATE && point_side != origin_side {
return Err(SphericalDelaunayValidationError::NonSupportingSimplex {
simplex_index,
simplex_vertices: simplex.vertex_indices().to_vec(),
point_index,
origin_side,
point_side,
});
}
}
}
Ok(())
}
const fn validate_ambient_dimension<const A: usize>()
-> Result<(), SphericalDelaunayValidationError> {
if A != D + 1 {
return Err(SphericalDelaunayValidationError::AmbientDimensionMismatch {
dimension: D,
expected: D + 1,
actual: A,
});
}
Ok(())
}
fn ambient_origin<const A: usize>() -> Result<Point<A>, SphericalDelaunayValidationError> {
Point::<A>::try_new([0.0; A]).map_err(|source| {
SphericalDelaunayValidationError::AmbientPointValidation {
point_index: None,
source,
}
})
}
fn simplex_origin_side<const A: usize>(
&self,
simplex_index: usize,
simplex: &SphericalSimplex<D>,
origin: Point<A>,
) -> Result<Orientation, SphericalDelaunayValidationError> {
let origin_side =
self.simplex_orientation_with_point::<A>(simplex_index, simplex, origin, None)?;
if origin_side == Orientation::DEGENERATE {
return Err(
SphericalDelaunayValidationError::SimplexHyperplaneContainsOrigin {
simplex_index,
simplex_vertices: simplex.vertex_indices().to_vec(),
},
);
}
Ok(origin_side)
}
fn ambient_point<const A: usize>(
&self,
point_index: usize,
) -> Result<Point<A>, SphericalDelaunayValidationError> {
let point = self.points.get(point_index).ok_or(
SphericalDelaunayValidationError::VertexIndexOutOfBounds {
vertex_index: point_index,
vertex_count: self.points.len(),
},
)?;
let coords = ambient_array_from_slice::<A>(point.coords()).map_err(|source| {
SphericalDelaunayValidationError::AmbientCoordinateArray {
point_index,
source,
}
})?;
Point::<A>::try_new(coords).map_err(|source| {
SphericalDelaunayValidationError::AmbientPointValidation {
point_index: Some(point_index),
source,
}
})
}
fn simplex_orientation_with_point<const A: usize>(
&self,
simplex_index: usize,
simplex: &SphericalSimplex<D>,
extra_point: Point<A>,
extra_point_index: Option<usize>,
) -> Result<Orientation, SphericalDelaunayValidationError> {
let mut simplex_points: SmallBuffer<Point<A>, MAX_PRACTICAL_DIMENSION_SIZE> =
SmallBuffer::with_capacity(A + 1);
for &point_index in simplex.vertex_indices() {
simplex_points.push(self.ambient_point::<A>(point_index)?);
}
simplex_points.push(extra_point);
simplex_orientation::<A>(&simplex_points).map_err(|source| {
SphericalDelaunayValidationError::SimplexOrientation {
simplex_index,
point_index: extra_point_index,
source,
}
})
}
}
#[derive(Clone, Debug)]
#[must_use = "call build to construct a spherical triangulation"]
pub struct SphericalDelaunayBuilder<const D: usize> {
points: Vec<SphericalPoint<D>>,
radius: f64,
construction_options: ConstructionOptions,
}
impl<const D: usize> SphericalDelaunayBuilder<D> {
pub fn try_new<I, P>(points: I) -> Result<Self, SphericalDelaunayConstructionError>
where
I: IntoIterator<Item = P>,
P: AsRef<[f64]>,
{
Self::try_new_with_radius(points, 1.0)
}
pub fn try_new_with_radius<I, P>(
points: I,
radius: f64,
) -> Result<Self, SphericalDelaunayConstructionError>
where
I: IntoIterator<Item = P>,
P: AsRef<[f64]>,
{
let metric = SphericalMetric::<D>::try_new(radius)
.map_err(|source| SphericalDelaunayConstructionError::Metric { source })?;
let normalized = points
.into_iter()
.enumerate()
.map(|(point_index, point)| {
metric.canonicalize_slice(point.as_ref()).map_err(|source| {
SphericalDelaunayConstructionError::Point {
point_index,
source,
}
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
points: normalized,
radius,
construction_options: ConstructionOptions::default(),
})
}
pub fn try_from_points(
points: Vec<SphericalPoint<D>>,
) -> Result<Self, SphericalDelaunayConstructionError> {
let radius = points.first().map_or(1.0, SphericalPoint::radius);
for (point_index, point) in points.iter().enumerate() {
if point.radius().to_bits() != radius.to_bits() {
return Err(SphericalDelaunayConstructionError::Point {
point_index,
source: SphericalPointError::MismatchedRadius {
expected: radius,
actual: point.radius(),
},
});
}
}
Ok(Self {
points,
radius,
construction_options: ConstructionOptions::default(),
})
}
pub const fn construction_options(mut self, options: ConstructionOptions) -> Self {
self.construction_options = options;
self
}
pub fn build(
self,
) -> Result<SphericalDelaunayTriangulation<D>, SphericalDelaunayConstructionError> {
match D {
2 => self.build_with_ambient::<3>(),
3 => self.build_with_ambient::<4>(),
_ => Err(SphericalDelaunayConstructionError::UnsupportedDimension {
dimension: D,
max_validated_dimension: 3,
tracking_issue: SPHERICAL_ROADMAP_ISSUE,
}),
}
}
fn build_with_ambient<const A: usize>(
self,
) -> Result<SphericalDelaunayTriangulation<D>, SphericalDelaunayConstructionError>
where
AdaptiveKernel<f64>: ExactPredicates<A>,
{
if A != D + 1 {
return Err(
SphericalDelaunayConstructionError::AmbientDimensionMismatch {
dimension: D,
expected: D + 1,
actual: A,
},
);
}
let minimum = A + 1;
if self.points.len() < minimum {
return Err(SphericalDelaunayConstructionError::InsufficientVertices {
dimension: D,
minimum,
actual: self.points.len(),
});
}
let ambient_vertices = self.ambient_vertices::<A>()?;
let ambient_options = self
.construction_options
.with_initial_simplex_strategy(InitialSimplexStrategy::Balanced);
let ambient: Triangulation<_, usize, (), A> =
DelaunayTriangulationBuilder::new(&ambient_vertices)
.topology_guarantee(TopologyGuarantee::Pseudomanifold)
.construction_options(ambient_options)
.build_triangulation()
.map_err(
|source| SphericalDelaunayConstructionError::AmbientConstruction {
source: Box::new(source),
},
)?;
let hull = ConvexHull::try_from_triangulation(&ambient).map_err(|source| {
SphericalDelaunayConstructionError::ConvexHull {
source: Box::new(source),
}
})?;
let origin = Point::<A>::try_new([0.0; A]).map_err(|source| {
SphericalDelaunayConstructionError::AmbientOriginValidation { source }
})?;
if hull.is_point_outside(&origin).map_err(|source| {
SphericalDelaunayConstructionError::ConvexHullQuery {
source: Box::new(source),
}
})? {
return Err(SphericalDelaunayConstructionError::OriginOutsideConvexHull);
}
let mut simplices = Vec::with_capacity(hull.number_of_facets());
for (simplex_index, facet) in hull.facets().enumerate() {
let mut simplex_vertices = Vec::with_capacity(D + 1);
for vertex in facet.vertices() {
let original_index = vertex.data().copied().ok_or(
SphericalDelaunayConstructionError::MissingAmbientVertexIndex { simplex_index },
)?;
simplex_vertices.push(original_index);
}
let simplex = SphericalSimplex::try_new(simplex_vertices, self.points.len()).map_err(
|source| SphericalDelaunayConstructionError::InvalidHullSimplex {
simplex_index,
source,
},
)?;
simplices.push(simplex);
}
if simplices.is_empty() {
return Err(SphericalDelaunayConstructionError::EmptyHull);
}
let spherical = SphericalDelaunayTriangulation {
points: self.points,
simplices,
radius: self.radius,
construction_provenance: SphericalConstructionProvenance::AmbientConvexHullBoundary,
};
spherical.validate_topology().map_err(|source| {
SphericalDelaunayConstructionError::TopologyValidation {
source: Box::new(source),
}
})?;
spherical.validate_realization().map_err(|source| {
SphericalDelaunayConstructionError::RealizationValidation {
source: Box::new(source),
}
})?;
spherical.validate_delaunay().map_err(|source| {
SphericalDelaunayConstructionError::DelaunayValidation {
source: Box::new(source),
}
})?;
Ok(spherical)
}
fn ambient_vertices<const A: usize>(
&self,
) -> Result<Vec<Vertex<usize, A>>, SphericalDelaunayConstructionError> {
self.points
.iter()
.enumerate()
.map(|(point_index, point)| {
let coords = ambient_array_from_slice::<A>(point.coords()).map_err(|source| {
SphericalDelaunayConstructionError::AmbientCoordinateArray {
point_index,
source,
}
})?;
vertex!(coords; data = point_index).map_err(|source| {
SphericalDelaunayConstructionError::AmbientVertex {
point_index,
source,
}
})
})
.collect()
}
}
fn synthetic_topology_coordinates<const D: usize>(
point_index: usize,
) -> Result<[f64; D], CoordinateConversionError> {
let base = safe_usize_to_scalar(point_index)? + 1.0;
let mut coords = [0.0; D];
for (axis, coord) in coords.iter_mut().enumerate() {
*coord = safe_usize_to_scalar(axis)?.mul_add(1.0e-3, base);
}
Ok(coords)
}
fn scaled_norm_over_radius(coords: &[f64], radius: f64) -> f64 {
let max_abs = coords
.iter()
.fold(0.0_f64, |max_abs, &coord| max_abs.max(coord.abs()));
if max_abs == 0.0 {
return 0.0;
}
let scaled_norm = coords
.iter()
.fold(0.0, |acc, &coord| {
let scaled = coord / max_abs;
scaled.mul_add(scaled, acc)
})
.sqrt();
(max_abs / radius) * scaled_norm
}
fn scaled_euclidean_norm(coords: &[f64]) -> f64 {
scaled_norm_over_radius(coords, 1.0)
}
fn intrinsic_tds_error(source: TdsError) -> SphericalDelaunayValidationError {
intrinsic_error(InvariantError::Tds { source })
}
fn intrinsic_manifold_error(source: ManifoldError) -> SphericalDelaunayValidationError {
intrinsic_error(InvariantError::from(source))
}
fn intrinsic_topology_error(
source: TriangulationValidationError,
) -> SphericalDelaunayValidationError {
intrinsic_error(InvariantError::Triangulation { source })
}
fn intrinsic_topology_support_error(source: TopologyError) -> SphericalDelaunayValidationError {
let invariant = match source {
TopologyError::FacetMapBuild { source }
| TopologyError::BoundaryFacetEnumeration { source }
| TopologyError::BoundaryFacetCount { source } => InvariantError::Tds { source },
TopologyError::BoundaryFacetSimplexAccess { source } => InvariantError::Tds {
source: source.into(),
},
TopologyError::BoundaryClassification { source } => InvariantError::from(*source),
};
intrinsic_error(invariant)
}
fn intrinsic_error(source: InvariantError) -> SphericalDelaunayValidationError {
SphericalDelaunayValidationError::IntrinsicTopology {
source: Box::new(source),
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum SphericalSimplexError {
#[error("spherical {dimension}D simplex vertex count is unrepresentable")]
UnrepresentableDimension {
dimension: usize,
},
#[error("spherical {dimension}D simplex requires {expected} vertices, got {actual}")]
InvalidArity {
dimension: usize,
expected: usize,
actual: usize,
},
#[error(
"spherical simplex vertex index {vertex_index} is out of bounds for {vertex_count} vertices"
)]
VertexIndexOutOfBounds {
vertex_index: usize,
vertex_count: usize,
},
#[error("spherical simplex contains duplicate vertex index {vertex_index}")]
DuplicateVertex {
vertex_index: usize,
},
}
#[derive(Clone, Debug, Error, PartialEq)]
#[non_exhaustive]
pub enum SphericalDelaunayConstructionError {
#[error("spherical metric configuration failed: {source}")]
Metric {
#[source]
source: SphericalPointError,
},
#[error("spherical point {point_index} failed canonicalization: {source}")]
Point {
point_index: usize,
#[source]
source: SphericalPointError,
},
#[error(
"spherical Delaunay construction is release-validated only up to {max_validated_dimension}D; {dimension}D support is tracked by issue #{tracking_issue}"
)]
UnsupportedDimension {
dimension: usize,
max_validated_dimension: usize,
tracking_issue: u32,
},
#[error(
"ambient dimension dispatch mismatch for S^{dimension}: expected {expected}, got {actual}"
)]
AmbientDimensionMismatch {
dimension: usize,
expected: usize,
actual: usize,
},
#[error(
"spherical S^{dimension} construction requires at least {minimum} points, got {actual}"
)]
InsufficientVertices {
dimension: usize,
minimum: usize,
actual: usize,
},
#[error("spherical point {point_index} could not be converted to an ambient array: {source}")]
AmbientCoordinateArray {
point_index: usize,
#[source]
source: SphericalPointError,
},
#[error("ambient vertex {point_index} failed construction: {source}")]
AmbientVertex {
point_index: usize,
#[source]
source: CoordinateConversionError,
},
#[error("ambient origin point failed validation: {source}")]
AmbientOriginValidation {
#[source]
source: CoordinateValidationError,
},
#[error("ambient convex-hull dual construction failed: {source}")]
AmbientConstruction {
#[source]
source: Box<DelaunayTriangulationConstructionError>,
},
#[error("ambient convex-hull operation failed: {source}")]
ConvexHull {
#[source]
source: Box<ConvexHullConstructionError>,
},
#[error("ambient convex-hull query failed: {source}")]
ConvexHullQuery {
#[source]
source: Box<ConvexHullQueryError>,
},
#[error("normalized spherical points do not enclose the origin in ambient space")]
OriginOutsideConvexHull,
#[error("ambient convex hull produced no facets")]
EmptyHull,
#[error("ambient hull simplex {simplex_index} was missing an original spherical vertex index")]
MissingAmbientVertexIndex {
simplex_index: usize,
},
#[error("ambient hull simplex {simplex_index} is not a valid spherical simplex: {source}")]
InvalidHullSimplex {
simplex_index: usize,
#[source]
source: SphericalSimplexError,
},
#[error("spherical topology validation failed after construction: {source}")]
TopologyValidation {
#[source]
source: Box<SphericalDelaunayValidationError>,
},
#[error("spherical realization validation failed after construction: {source}")]
RealizationValidation {
#[source]
source: Box<SphericalDelaunayValidationError>,
},
#[error("spherical Delaunay validation failed after construction: {source}")]
DelaunayValidation {
#[source]
source: Box<SphericalDelaunayValidationError>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SphericalValidationLayer {
Realization,
Delaunay,
}
impl fmt::Display for SphericalValidationLayer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Realization => f.write_str("Level 4 Spherical Realization"),
Self::Delaunay => f.write_str("Level 5 Geometric Predicates"),
}
}
}
#[derive(Clone, Debug, Error, PartialEq)]
#[non_exhaustive]
pub enum SphericalDelaunayValidationError {
#[error("spherical metric configuration failed: {source}")]
Metric {
#[source]
source: SphericalPointError,
},
#[error("geodesic distance between spherical vertices {left} and {right} failed: {source}")]
GeodesicDistance {
left: usize,
right: usize,
#[source]
source: SphericalPointError,
},
#[error("vertex index {vertex_index} is out of bounds for {vertex_count} spherical vertices")]
VertexIndexOutOfBounds {
vertex_index: usize,
vertex_count: usize,
},
#[error("spherical triangulation contains no simplices")]
NoSimplices,
#[error("spherical point {point_index} has {actual} ambient coordinates, expected {expected}")]
InvalidPointAmbientDimension {
point_index: usize,
expected: usize,
actual: usize,
},
#[error("spherical point {point_index} has norm {norm:?}, expected radius {radius:?}")]
PointNotOnSphere {
point_index: usize,
radius: f64,
norm: f64,
},
#[error("spherical simplex {simplex_index} is invalid: {source}")]
InvalidSimplex {
simplex_index: usize,
#[source]
source: SphericalSimplexError,
},
#[error(
"spherical simplex {simplex_index} duplicates simplex {previous_simplex_index} with vertices {simplex_vertices:?}"
)]
DuplicateSimplex {
simplex_index: usize,
previous_simplex_index: usize,
simplex_vertices: Vec<usize>,
},
#[error("spherical vertex {vertex_index} is not incident to any simplex")]
UnusedVertex {
vertex_index: usize,
},
#[error("spherical facet {facet_vertices:?} has incident count {incident_count}, expected 2")]
InvalidFacetIncidence {
facet_vertices: Vec<usize>,
incident_count: usize,
},
#[error("abstract spherical topology vertex {point_index} failed construction: {source}")]
AbstractTopologyVertex {
point_index: usize,
#[source]
source: CoordinateConversionError,
},
#[error("abstract spherical topology vertex {point_index} failed insertion: {source}")]
AbstractTopologyVertexInsertion {
point_index: usize,
#[source]
source: Box<TdsConstructionError>,
},
#[error("abstract spherical topology simplex {simplex_index} failed construction: {source}")]
AbstractTopologySimplex {
simplex_index: usize,
#[source]
source: SimplexValidationError,
},
#[error("abstract spherical topology simplex {simplex_index} failed insertion: {source}")]
AbstractTopologySimplexInsertion {
simplex_index: usize,
#[source]
source: Box<TdsConstructionError>,
},
#[error("abstract spherical topology mutation failed: {source}")]
AbstractTopologyMutation {
#[source]
source: TdsMutationError,
},
#[error("spherical intrinsic PL topology validation failed: {source}")]
IntrinsicTopology {
#[source]
source: Box<InvariantError>,
},
#[error(
"{layer} is not defined for the spherical prototype in {dimension}D; full support is tracked by issue #{tracking_issue}"
)]
UnsupportedLayer {
layer: SphericalValidationLayer,
dimension: usize,
tracking_issue: u32,
},
#[error(
"ambient dimension dispatch mismatch for S^{dimension}: expected {expected}, got {actual}"
)]
AmbientDimensionMismatch {
dimension: usize,
expected: usize,
actual: usize,
},
#[error("spherical point {point_index} could not be converted to an ambient array: {source}")]
AmbientCoordinateArray {
point_index: usize,
#[source]
source: SphericalPointError,
},
#[error(
"ambient point {point_index:?} failed validation during spherical validation: {source}"
)]
AmbientPointValidation {
point_index: Option<usize>,
#[source]
source: CoordinateValidationError,
},
#[error(
"orientation failed for spherical simplex {simplex_index} with test point {point_index:?}: {source}"
)]
SimplexOrientation {
simplex_index: usize,
point_index: Option<usize>,
#[source]
source: CoordinateConversionError,
},
#[error(
"spherical simplex {simplex_index} has a supporting hyperplane through the origin: {simplex_vertices:?}"
)]
SimplexHyperplaneContainsOrigin {
simplex_index: usize,
simplex_vertices: Vec<usize>,
},
#[error(
"spherical simplex {simplex_index} is not a supporting hull facet: point {point_index} has side {point_side:?}, origin side is {origin_side:?}, vertices {simplex_vertices:?}"
)]
NonSupportingSimplex {
simplex_index: usize,
simplex_vertices: Vec<usize>,
point_index: usize,
origin_side: Orientation,
point_side: Orientation,
},
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use super::*;
use crate::tds::FacetError;
fn spherical_triangle(vertices: [usize; 3], vertex_count: usize) -> SphericalSimplex<2> {
SphericalSimplex::<2>::try_new(vertices.to_vec(), vertex_count)
.expect("fixture simplex should be well-formed")
}
fn tetrahedron_boundary_points() -> Vec<SphericalPoint<2>> {
tetrahedron_boundary_points_with_radius(1.0)
}
fn tetrahedron_boundary_points_with_radius(radius: f64) -> Vec<SphericalPoint<2>> {
vec![
SphericalPoint::<2>::try_new_with_radius([1.0, 1.0, 1.0], radius)
.expect("finite point"),
SphericalPoint::<2>::try_new_with_radius([1.0, -1.0, -1.0], radius)
.expect("finite point"),
SphericalPoint::<2>::try_new_with_radius([-1.0, 1.0, -1.0], radius)
.expect("finite point"),
SphericalPoint::<2>::try_new_with_radius([-1.0, -1.0, 1.0], radius)
.expect("finite point"),
]
}
fn tetrahedron_boundary_simplices(vertex_count: usize) -> Vec<SphericalSimplex<2>> {
vec![
spherical_triangle([0, 1, 2], vertex_count),
spherical_triangle([0, 1, 3], vertex_count),
spherical_triangle([0, 2, 3], vertex_count),
spherical_triangle([1, 2, 3], vertex_count),
]
}
fn check_spherical_simplex_arity<const D: usize>() {
let expected = D + 1;
let indices: Vec<_> = (0..expected).collect();
let simplex = SphericalSimplex::<D>::try_new(indices.clone(), expected).unwrap();
assert_eq!(simplex.vertex_indices(), indices);
assert_eq!(simplex.dimension(), D);
assert_eq!(
SphericalSimplex::<D>::try_new(Vec::new(), expected),
Err(SphericalSimplexError::InvalidArity {
dimension: D,
expected,
actual: 0,
})
);
}
macro_rules! spherical_simplex_arity_tests {
($($dimension:literal),+ $(,)?) => {
pastey::paste! {
$(
#[test]
fn [<spherical_simplex_arity_ $dimension d>]() {
check_spherical_simplex_arity::<$dimension>();
}
)+
}
};
}
spherical_simplex_arity_tests!(2, 3, 4, 5);
#[test]
fn spherical_simplex_rejects_unrepresentable_dimension() {
assert_eq!(
SphericalSimplex::<{ usize::MAX }>::try_new(Vec::new(), 0),
Err(SphericalSimplexError::UnrepresentableDimension {
dimension: usize::MAX
})
);
}
#[test]
fn intrinsic_topology_adapters_preserve_typed_tds_sources() {
let direct = intrinsic_tds_error(TdsError::InconsistentDataStructure {
message: "invalid synthetic adjacency".to_string(),
});
assert_matches!(
direct,
SphericalDelaunayValidationError::IntrinsicTopology { source }
if matches!(
source.as_ref(),
InvariantError::Tds {
source: TdsError::InconsistentDataStructure { message }
} if message == "invalid synthetic adjacency"
)
);
let boundary_count = intrinsic_topology_support_error(TopologyError::BoundaryFacetCount {
source: TdsError::InconsistentDataStructure {
message: "invalid boundary count".to_string(),
},
});
assert_matches!(
boundary_count,
SphericalDelaunayValidationError::IntrinsicTopology { source }
if matches!(
source.as_ref(),
InvariantError::Tds {
source: TdsError::InconsistentDataStructure { message }
} if message == "invalid boundary count"
)
);
let facet_access =
intrinsic_topology_support_error(TopologyError::BoundaryFacetSimplexAccess {
source: FacetError::FacetNotFoundInTriangulation,
});
assert_matches!(
facet_access,
SphericalDelaunayValidationError::IntrinsicTopology { source }
if matches!(
source.as_ref(),
InvariantError::Tds {
source: TdsError::FacetError {
source: FacetError::FacetNotFoundInTriangulation
}
}
)
);
}
#[test]
fn level3_rejects_empty_spherical_complex() {
let triangulation = SphericalDelaunayTriangulation::<2> {
points: tetrahedron_boundary_points(),
simplices: Vec::new(),
radius: 1.0,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
assert_matches!(
triangulation.validate_topology(),
Err(SphericalDelaunayValidationError::NoSimplices)
);
}
#[test]
fn level3_rejects_duplicate_spherical_simplices() {
let points = tetrahedron_boundary_points();
let vertex_count = points.len();
let triangulation = SphericalDelaunayTriangulation::<2> {
points,
simplices: vec![
spherical_triangle([0, 1, 2], vertex_count),
spherical_triangle([2, 1, 0], vertex_count),
],
radius: 1.0,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
assert_matches!(
triangulation.validate_topology(),
Err(SphericalDelaunayValidationError::DuplicateSimplex {
simplex_index: 1,
previous_simplex_index: 0,
simplex_vertices,
}) if simplex_vertices == vec![0, 1, 2]
);
}
#[test]
fn level3_rejects_malformed_stored_spherical_simplex() {
let points = tetrahedron_boundary_points();
let triangulation = SphericalDelaunayTriangulation::<2> {
points,
simplices: vec![SphericalSimplex {
vertices: vec![0, 1, 4],
}],
radius: 1.0,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
assert_matches!(
triangulation.validate_topology(),
Err(SphericalDelaunayValidationError::InvalidSimplex {
simplex_index: 0,
source: SphericalSimplexError::VertexIndexOutOfBounds {
vertex_index: 4,
vertex_count: 4,
},
})
);
}
#[test]
fn level3_rejects_open_spherical_facet_incidence() {
let points = tetrahedron_boundary_points();
let vertex_count = points.len();
let triangulation = SphericalDelaunayTriangulation::<2> {
points,
simplices: vec![spherical_triangle([0, 1, 2], vertex_count)],
radius: 1.0,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
assert_matches!(
triangulation.validate_topology(),
Err(SphericalDelaunayValidationError::InvalidFacetIncidence {
incident_count: 1,
..
})
);
}
#[test]
fn level3_rejects_unused_spherical_vertices() {
let mut points = tetrahedron_boundary_points();
points.push(SphericalPoint::<2>::try_new([1.0, 0.0, 0.0]).expect("finite point"));
let vertex_count = points.len();
let triangulation = SphericalDelaunayTriangulation::<2> {
points,
simplices: tetrahedron_boundary_simplices(vertex_count),
radius: 1.0,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
assert_matches!(
triangulation.validate_topology(),
Err(SphericalDelaunayValidationError::UnusedVertex { vertex_index: 4 })
);
}
#[test]
fn level3_rejects_points_not_on_configured_radius() {
let points = tetrahedron_boundary_points();
let vertex_count = points.len();
let triangulation = SphericalDelaunayTriangulation::<2> {
points,
simplices: tetrahedron_boundary_simplices(vertex_count),
radius: 2.0,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
assert_matches!(
triangulation.validate_topology(),
Err(SphericalDelaunayValidationError::PointNotOnSphere {
point_index: 0,
radius,
norm,
}) if radius.to_bits() == 2.0_f64.to_bits() && (norm - 1.0).abs() < 1.0e-12
);
}
#[test]
fn distance_reports_typed_geodesic_failure() {
let unit =
SphericalPoint::<2>::try_new([1.0, 0.0, 0.0]).expect("unit point should normalize");
let radius_two = SphericalPoint::<2>::try_new_with_radius([0.0, 1.0, 0.0], 2.0)
.expect("radius-two point should normalize");
let triangulation = SphericalDelaunayTriangulation::<2> {
points: vec![unit, radius_two],
simplices: Vec::new(),
radius: 1.0,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
assert_matches!(
triangulation.distance_between_vertices(0, 1),
Err(SphericalDelaunayValidationError::GeodesicDistance {
left: 0,
right: 1,
source: SphericalPointError::MismatchedRadius { expected, actual },
}) if expected.to_bits() == 1.0_f64.to_bits()
&& actual.to_bits() == 2.0_f64.to_bits()
);
}
#[test]
fn level3_accepts_large_radius_without_norm_overflow() {
let radius = f64::MAX / 4.0;
let points = tetrahedron_boundary_points_with_radius(radius);
let vertex_count = points.len();
let triangulation = SphericalDelaunayTriangulation::<2> {
points,
simplices: tetrahedron_boundary_simplices(vertex_count),
radius,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
triangulation
.validate_topology()
.expect("scaled norm check should avoid squaring large finite radii");
}
#[test]
fn level4_rejects_s4_after_intrinsic_topology_passes() {
let points = vec![
SphericalPoint::<4>::try_new([1.0, 0.0, 0.0, 0.0, 0.0]).expect("finite point"),
SphericalPoint::<4>::try_new([0.0, 1.0, 0.0, 0.0, 0.0]).expect("finite point"),
SphericalPoint::<4>::try_new([0.0, 0.0, 1.0, 0.0, 0.0]).expect("finite point"),
SphericalPoint::<4>::try_new([0.0, 0.0, 0.0, 1.0, 0.0]).expect("finite point"),
SphericalPoint::<4>::try_new([0.0, 0.0, 0.0, 0.0, 1.0]).expect("finite point"),
SphericalPoint::<4>::try_new([-1.0, -1.0, -1.0, -1.0, -1.0]).expect("finite point"),
];
let vertex_count = points.len();
let simplices = (0..vertex_count)
.map(|omitted| {
let vertices = (0..vertex_count)
.filter(|&vertex_index| vertex_index != omitted)
.collect();
SphericalSimplex::<4>::try_new(vertices, vertex_count)
.expect("boundary simplex should be well formed")
})
.collect();
let triangulation = SphericalDelaunayTriangulation::<4> {
points,
simplices,
radius: 1.0,
construction_provenance: SphericalConstructionProvenance::AmbientConvexHullBoundary,
};
triangulation
.validate_topology()
.expect("5-simplex boundary is intrinsically S^4");
assert_matches!(
triangulation.validate_realization(),
Err(SphericalDelaunayValidationError::UnsupportedLayer {
layer: SphericalValidationLayer::Realization,
dimension: 4,
tracking_issue: SPHERICAL_ROADMAP_ISSUE,
})
);
}
#[test]
fn level5_rejects_topological_flip_that_is_not_a_hull_facet() {
let points = vec![
SphericalPoint::<2>::try_new([1.0, 1.0, 1.0]).expect("finite point"),
SphericalPoint::<2>::try_new([1.0, -1.0, -1.0]).expect("finite point"),
SphericalPoint::<2>::try_new([-1.0, 1.0, -1.0]).expect("finite point"),
SphericalPoint::<2>::try_new([-1.0, -1.0, 1.0]).expect("finite point"),
SphericalPoint::<2>::try_new([1.0, 0.9, -0.8]).expect("finite point"),
];
let vertex_count = points.len();
let triangulation = SphericalDelaunayTriangulation::<2> {
points,
simplices: vec![
spherical_triangle([0, 3, 4], vertex_count),
spherical_triangle([1, 3, 4], vertex_count),
spherical_triangle([1, 2, 4], vertex_count),
spherical_triangle([2, 0, 4], vertex_count),
spherical_triangle([0, 2, 3], vertex_count),
spherical_triangle([1, 2, 3], vertex_count),
],
radius: 1.0,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
triangulation
.validate_topology()
.expect("flipped fixture remains a closed PL sphere");
triangulation
.validate_realization()
.expect("flipped fixture remains a spherical realization");
assert_matches!(
triangulation.validate_delaunay(),
Err(SphericalDelaunayValidationError::NonSupportingSimplex { .. })
);
}
#[test]
fn level4_rejects_closed_topology_with_simplex_hyperplane_through_origin() {
let points = vec![
SphericalPoint::<2>::try_new([1.0, 0.0, 0.0]).expect("finite point"),
SphericalPoint::<2>::try_new([-1.0, 0.0, 0.0]).expect("finite point"),
SphericalPoint::<2>::try_new([0.0, 1.0, 0.0]).expect("finite point"),
SphericalPoint::<2>::try_new([0.0, 0.0, 1.0]).expect("finite point"),
];
let vertex_count = points.len();
let triangulation = SphericalDelaunayTriangulation::<2> {
points,
simplices: vec![
spherical_triangle([0, 1, 2], vertex_count),
spherical_triangle([0, 1, 3], vertex_count),
spherical_triangle([0, 2, 3], vertex_count),
spherical_triangle([1, 2, 3], vertex_count),
],
radius: 1.0,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
triangulation
.validate_topology()
.expect("fixture is combinatorially a closed sphere");
assert_matches!(
triangulation.validate_realization(),
Err(
SphericalDelaunayValidationError::SimplexHyperplaneContainsOrigin {
simplex_index: 0,
..
}
)
);
}
#[test]
fn level3_rejects_closed_facet_counts_with_disconnected_vertex_wedge() {
let points = vec![
SphericalPoint::<2>::try_new([1.0, 0.0, 0.0]).expect("finite point"),
SphericalPoint::<2>::try_new([0.0, 1.0, 0.0]).expect("finite point"),
SphericalPoint::<2>::try_new([0.0, 0.0, 1.0]).expect("finite point"),
SphericalPoint::<2>::try_new([-1.0, -1.0, -1.0]).expect("finite point"),
SphericalPoint::<2>::try_new([0.0, -1.0, 0.0]).expect("finite point"),
SphericalPoint::<2>::try_new([0.0, 0.0, -1.0]).expect("finite point"),
SphericalPoint::<2>::try_new([-1.0, 1.0, 1.0]).expect("finite point"),
];
let vertex_count = points.len();
let triangulation = SphericalDelaunayTriangulation::<2> {
points,
simplices: vec![
spherical_triangle([0, 1, 2], vertex_count),
spherical_triangle([0, 1, 3], vertex_count),
spherical_triangle([0, 2, 3], vertex_count),
spherical_triangle([1, 2, 3], vertex_count),
spherical_triangle([0, 4, 5], vertex_count),
spherical_triangle([0, 4, 6], vertex_count),
spherical_triangle([0, 5, 6], vertex_count),
spherical_triangle([4, 5, 6], vertex_count),
],
radius: 1.0,
construction_provenance: SphericalConstructionProvenance::Unproven,
};
assert_matches!(
triangulation.validate_topology(),
Err(SphericalDelaunayValidationError::IntrinsicTopology { source })
if matches!(
source.as_ref(),
InvariantError::Triangulation { source:
TriangulationValidationError::Disconnected { simplex_count: 8 }
}
)
);
}
}