#![forbid(unsafe_code)]
use crate::core::{
collections::{MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer},
tds::{SimplexKey, TdsError, VertexKey},
triangulation::Triangulation,
};
use crate::geometry::{
kernel::Kernel,
point::Point,
traits::coordinate::CoordinateConversionValue,
util::{CircumcenterError, circumradius, hypot, inradius as simplex_inradius, simplex_volume},
};
use core::{array, fmt};
use num_traits::One;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum QualityNumericOperation {
EpsilonFloorConversion,
EdgeCountConversion,
RelativeFactorConversion,
Circumradius,
Inradius,
Volume,
}
impl fmt::Display for QualityNumericOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EpsilonFloorConversion => f.write_str("epsilon floor conversion"),
Self::EdgeCountConversion => f.write_str("edge count conversion"),
Self::RelativeFactorConversion => f.write_str("relative factor conversion"),
Self::Circumradius => f.write_str("circumradius computation"),
Self::Inradius => f.write_str("inradius computation"),
Self::Volume => f.write_str("simplex volume computation"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum QualityDegeneracyMeasure {
Inradius,
Volume,
AverageEdgeLength,
EdgeLengthPower,
}
impl fmt::Display for QualityDegeneracyMeasure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Inradius => f.write_str("inradius"),
Self::Volume => f.write_str("volume"),
Self::AverageEdgeLength => f.write_str("average edge length"),
Self::EdgeLengthPower => f.write_str("edge length power"),
}
}
}
#[derive(Debug, Clone, PartialEq, Error)]
#[non_exhaustive]
pub enum QualitySimplexVerticesError {
#[error("Simplex key {simplex_key:?} not found: {context}")]
SimplexNotFound {
simplex_key: SimplexKey,
context: String,
},
#[error("Vertex key {vertex_key:?} referenced by the simplex was not found: {context}")]
ReferencedVertexNotFound {
vertex_key: VertexKey,
context: String,
},
#[error("Unexpected TDS failure while extracting quality metric simplex vertices: {source}")]
UnexpectedTdsFailure {
#[source]
source: Box<TdsError>,
},
}
impl From<TdsError> for QualitySimplexVerticesError {
fn from(source: TdsError) -> Self {
match source {
TdsError::SimplexNotFound {
simplex_key,
context,
} => Self::SimplexNotFound {
simplex_key,
context,
},
TdsError::VertexNotFound {
vertex_key,
context,
} => Self::ReferencedVertexNotFound {
vertex_key,
context,
},
other => Self::UnexpectedTdsFailure {
source: Box::new(other),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Error)]
#[non_exhaustive]
pub enum QualityError {
#[error("Failed to fetch vertices for simplex {simplex_key:?}: {source}")]
SimplexVertices {
simplex_key: SimplexKey,
#[source]
source: QualitySimplexVerticesError,
},
#[error("Vertex {vertex_key:?} referenced by a quality metric simplex was not found")]
VertexNotFound {
vertex_key: VertexKey,
},
#[error("Simplex has {actual} vertices, expected {expected} for dimension {dimension}")]
InvalidSimplexArity {
actual: usize,
expected: usize,
dimension: usize,
},
#[error("Numeric conversion failed during {operation}")]
NumericConversion {
operation: QualityNumericOperation,
},
#[error("Circumradius computation failed: {source}")]
Circumradius {
#[source]
source: Box<CircumcenterError>,
},
#[error("Inradius computation failed: {source}")]
Inradius {
#[source]
source: Box<CircumcenterError>,
},
#[error("Volume computation failed: {source}")]
Volume {
#[source]
source: Box<CircumcenterError>,
},
#[error(
"Degenerate simplex: {measure} observed={observed}, epsilon={epsilon}, avg_edge_length={avg_edge_length:?}"
)]
DegenerateSimplex {
measure: QualityDegeneracyMeasure,
observed: CoordinateConversionValue,
epsilon: CoordinateConversionValue,
avg_edge_length: Option<CoordinateConversionValue>,
},
}
fn simplex_points<K, U, V, const D: usize>(
tri: &Triangulation<K, U, V, D>,
simplex_key: SimplexKey,
) -> Result<SmallBuffer<Point<D>, MAX_PRACTICAL_DIMENSION_SIZE>, QualityError>
where
K: Kernel<D, Scalar = f64>,
{
let vertex_keys =
tri.tds
.simplex_vertices(simplex_key)
.map_err(|source| QualityError::SimplexVertices {
simplex_key,
source: source.into(),
})?;
let mut points = SmallBuffer::new();
for &vkey in vertex_keys {
let point = tri
.tds
.vertex(vkey)
.map(|v| *v.point())
.ok_or(QualityError::VertexNotFound { vertex_key: vkey })?;
points.push(point);
}
Ok(points)
}
fn scale_aware_epsilon<const D: usize>(
points: &SmallBuffer<Point<D>, MAX_PRACTICAL_DIMENSION_SIZE>,
) -> (f64, f64) {
let (total_edge_length, edge_count) = points
.iter()
.enumerate()
.flat_map(|(i, point_i)| {
points.iter().skip(i + 1).map(move |point_j| {
let diff_coords: [f64; D] =
array::from_fn(|idx| point_i.coords()[idx] - point_j.coords()[idx]);
hypot(&diff_coords)
})
})
.fold((0.0, 0_u32), |(total, count), dist| {
(total + dist, count + 1)
});
if edge_count == 0 {
return (0.0, 1e-12);
}
let avg_edge_length = total_edge_length / f64::from(edge_count);
let floor: f64 = 1e-12;
let relative_factor: f64 = 1e-8;
let epsilon = floor.max(avg_edge_length * relative_factor);
(avg_edge_length, epsilon)
}
pub fn radius_ratio<K, U, V, const D: usize>(
tri: &Triangulation<K, U, V, D>,
simplex_key: SimplexKey,
) -> Result<f64, QualityError>
where
K: Kernel<D, Scalar = f64>,
{
let points = simplex_points(tri, simplex_key)?;
if points.len() != D + 1 {
return Err(QualityError::InvalidSimplexArity {
actual: points.len(),
expected: D + 1,
dimension: D,
});
}
let circumradius_val = circumradius(&points).map_err(|source| QualityError::Circumradius {
source: Box::new(source),
})?;
let inradius_val = simplex_inradius(&points).map_err(|source| QualityError::Inradius {
source: Box::new(source),
})?;
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points);
if inradius_val < epsilon {
return Err(QualityError::DegenerateSimplex {
measure: QualityDegeneracyMeasure::Inradius,
observed: CoordinateConversionValue::from_numeric_debug(&inradius_val),
epsilon: CoordinateConversionValue::from_numeric_debug(&epsilon),
avg_edge_length: Some(CoordinateConversionValue::from_numeric_debug(
&avg_edge_length,
)),
});
}
let ratio = circumradius_val / inradius_val;
Ok(ratio)
}
pub fn normalized_volume<K, U, V, const D: usize>(
tri: &Triangulation<K, U, V, D>,
simplex_key: SimplexKey,
) -> Result<f64, QualityError>
where
K: Kernel<D, Scalar = f64>,
{
let points = simplex_points(tri, simplex_key)?;
if points.len() != D + 1 {
return Err(QualityError::InvalidSimplexArity {
actual: points.len(),
expected: D + 1,
dimension: D,
});
}
let volume = simplex_volume(&points).map_err(|source| QualityError::Volume {
source: Box::new(source),
})?;
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points);
let mut epsilon_pow = f64::one();
for _ in 0..D {
epsilon_pow *= epsilon;
}
if volume < epsilon_pow {
return Err(QualityError::DegenerateSimplex {
measure: QualityDegeneracyMeasure::Volume,
observed: CoordinateConversionValue::from_numeric_debug(&volume),
epsilon: CoordinateConversionValue::from_numeric_debug(&epsilon_pow),
avg_edge_length: Some(CoordinateConversionValue::from_numeric_debug(
&avg_edge_length,
)),
});
}
if avg_edge_length < epsilon {
return Err(QualityError::DegenerateSimplex {
measure: QualityDegeneracyMeasure::AverageEdgeLength,
observed: CoordinateConversionValue::from_numeric_debug(&avg_edge_length),
epsilon: CoordinateConversionValue::from_numeric_debug(&epsilon),
avg_edge_length: Some(CoordinateConversionValue::from_numeric_debug(
&avg_edge_length,
)),
});
}
let mut edge_length_power = f64::one();
for _ in 0..D {
edge_length_power *= avg_edge_length;
}
if edge_length_power < epsilon_pow {
return Err(QualityError::DegenerateSimplex {
measure: QualityDegeneracyMeasure::EdgeLengthPower,
observed: CoordinateConversionValue::from_numeric_debug(&edge_length_power),
epsilon: CoordinateConversionValue::from_numeric_debug(&epsilon_pow),
avg_edge_length: Some(CoordinateConversionValue::from_numeric_debug(
&avg_edge_length,
)),
});
}
let normalized = volume / edge_length_power;
Ok(normalized)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::construction::{
DelaunayConstructionFailure, DelaunayTriangulationConstructionError,
};
use crate::core::simplex::Simplex;
use crate::core::tds::Tds;
use crate::core::triangulation::Triangulation;
use crate::geometry::kernel::FastKernel;
use crate::triangulation::DelaunayTriangulation;
use crate::vertex;
use approx::assert_relative_eq;
use std::{array, assert_matches};
use slotmap::KeyData;
fn scalar_payload(value: &CoordinateConversionValue) -> f64 {
let CoordinateConversionValue::Scalar(value) = value else {
panic!("expected scalar diagnostic payload, got {value:?}");
};
value.get()
}
macro_rules! test_quality_dimensions {
($(
$test_name:ident => $dim:expr => $desc:expr =>
$vertices:expr,
$expected_ratio_min:expr,
$expected_ratio_max:expr
),+ $(,)?) => {
$(
#[test]
fn $test_name() {
let vertices = $vertices;
let dt: DelaunayTriangulation<_, (), (), $dim> = DelaunayTriangulation::builder(&vertices).build().unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
let ratio = radius_ratio(dt.as_triangulation(), simplex_key).unwrap();
assert!(
($expected_ratio_min..=$expected_ratio_max).contains(&ratio),
"{}D {}: radius_ratio={ratio}, expected range [{}, {}]",
$dim, $desc, $expected_ratio_min, $expected_ratio_max
);
let norm_vol = normalized_volume(dt.as_triangulation(), simplex_key).unwrap();
assert!(norm_vol > 0.0, "{}D {}: normalized_volume should be positive", $dim, $desc);
}
pastey::paste! {
#[test]
fn [<$test_name _scale_invariance>]() {
let vertices_base = $vertices;
let dt_base: DelaunayTriangulation<_, (), (), $dim> = DelaunayTriangulation::builder(&vertices_base).build().unwrap();
let key_base = dt_base.simplices().next().unwrap().0;
let vertices_scaled: Vec<_> = vertices_base.iter().map(|v| {
let coords: [f64; $dim] = array::from_fn(|idx| v.point().coords()[idx] * 10.0);
vertex!(coords).unwrap()
}).collect();
let dt_scaled: DelaunayTriangulation<_, (), (), $dim> = DelaunayTriangulation::builder(&vertices_scaled).build().unwrap();
let key_scaled = dt_scaled.simplices().next().unwrap().0;
let ratio_base = radius_ratio(dt_base.as_triangulation(), key_base).unwrap();
let ratio_scaled = radius_ratio(dt_scaled.as_triangulation(), key_scaled).unwrap();
assert_relative_eq!(ratio_base, ratio_scaled, epsilon = 1e-8);
let vol_base = normalized_volume(dt_base.as_triangulation(), key_base).unwrap();
let vol_scaled = normalized_volume(dt_scaled.as_triangulation(), key_scaled).unwrap();
assert_relative_eq!(vol_base, vol_scaled, epsilon = 1e-5);
}
#[test]
fn [<$test_name _translation_invariance>]() {
let vertices_base = $vertices;
let dt_base: DelaunayTriangulation<_, (), (), $dim> = DelaunayTriangulation::builder(&vertices_base).build().unwrap();
let key_base = dt_base.simplices().next().unwrap().0;
let vertices_translated: Vec<_> = vertices_base.iter().map(|v| {
let coords: [f64; $dim] = array::from_fn(|idx| v.point().coords()[idx] + 5.0);
vertex!(coords).unwrap()
}).collect();
let dt_translated: DelaunayTriangulation<_, (), (), $dim> = DelaunayTriangulation::builder(&vertices_translated).build().unwrap();
let key_translated = dt_translated.simplices().next().unwrap().0;
let ratio_base = radius_ratio(dt_base.as_triangulation(), key_base).unwrap();
let ratio_translated = radius_ratio(dt_translated.as_triangulation(), key_translated).unwrap();
assert_relative_eq!(ratio_base, ratio_translated, epsilon = 1e-10);
let vol_base = normalized_volume(dt_base.as_triangulation(), key_base).unwrap();
let vol_translated = normalized_volume(dt_translated.as_triangulation(), key_translated).unwrap();
assert_relative_eq!(vol_base, vol_translated, epsilon = 1e-10);
}
}
)+
};
}
test_quality_dimensions! {
quality_2d_unit => 2 => "unit simplex" =>
vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.0, 1.0]).unwrap(),
],
2.0, 3.0,
quality_2d_equilateral => 2 => "equilateral triangle" =>
vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025]).unwrap(),
],
1.9, 2.1,
quality_2d_right => 2 => "right triangle" =>
vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([3.0, 0.0]).unwrap(),
vertex!([0.0, 4.0]).unwrap(),
],
2.0, 5.0,
quality_3d_unit => 3 => "unit simplex" =>
vec![
vertex!([0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 1.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 1.0]).unwrap(),
],
3.0, 5.0,
quality_3d_regular => 3 => "regular tetrahedron" =>
vec![
vertex!([0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025, 0.0]).unwrap(),
vertex!([0.5, 0.288_675, 0.816_497]).unwrap(),
],
2.8, 3.2,
quality_4d_unit => 4 => "unit simplex" =>
vec![
vertex!([0.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 1.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 1.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 0.0, 1.0]).unwrap(),
],
4.0, 7.0,
quality_4d_regular => 4 => "regular simplex" =>
vec![
vertex!([0.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025, 0.0, 0.0]).unwrap(),
vertex!([0.5, 0.288_675, 0.816_497, 0.0]).unwrap(),
vertex!([0.5, 0.288_675, 0.204_124, 0.790_569]).unwrap(),
],
3.8, 4.2,
quality_5d_unit => 5 => "unit simplex" =>
vec![
vertex!([0.0, 0.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 1.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 1.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 0.0, 1.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 0.0, 0.0, 1.0]).unwrap(),
],
5.0, 15.0,
}
#[test]
fn test_degenerate_nearly_collinear() {
let vertices = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([2.0, 0.001]).unwrap(),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
let ratio_result = radius_ratio(dt.as_triangulation(), simplex_key);
if let Ok(ratio) = ratio_result {
assert!(ratio > 10.0);
} else {
assert_matches!(ratio_result, Err(QualityError::DegenerateSimplex { .. }));
}
let vol_result = normalized_volume(dt.as_triangulation(), simplex_key);
if let Ok(norm_vol) = vol_result {
assert!(norm_vol < 0.01);
} else {
assert_matches!(vol_result, Err(QualityError::DegenerateSimplex { .. }));
}
}
#[test]
fn quality_simplex_vertices_error_preserves_specific_tds_lookup_failures() {
let simplex_key = SimplexKey::from(KeyData::from_ffi(0xCAFE));
let vertex_key = VertexKey::from(KeyData::from_ffi(0xBEEF));
let missing_simplex = QualitySimplexVerticesError::from(TdsError::SimplexNotFound {
simplex_key,
context: "quality metric simplex lookup".to_string(),
});
assert_matches!(
missing_simplex,
QualitySimplexVerticesError::SimplexNotFound {
simplex_key: observed,
context
} if observed == simplex_key && context == "quality metric simplex lookup"
);
let missing_vertex = QualitySimplexVerticesError::from(TdsError::VertexNotFound {
vertex_key,
context: "quality metric vertex lookup".to_string(),
});
assert_matches!(
missing_vertex,
QualitySimplexVerticesError::ReferencedVertexNotFound {
vertex_key: observed,
context
} if observed == vertex_key && context == "quality metric vertex lookup"
);
let unexpected = QualitySimplexVerticesError::from(TdsError::DuplicateSimplices {
message: "same vertex set appears twice".to_string(),
});
assert_matches!(
unexpected,
QualitySimplexVerticesError::UnexpectedTdsFailure { source }
if matches!(
*source,
TdsError::DuplicateSimplices { ref message }
if message == "same vertex set appears twice"
)
);
}
#[test]
fn scale_aware_epsilon_uses_floor_when_point_set_has_no_edges() {
let mut points = SmallBuffer::new();
points.push(Point::try_new([]).expect("finite point coordinates"));
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points);
assert_relative_eq!(avg_edge_length, 0.0);
assert_relative_eq!(epsilon, 1e-12);
}
#[test]
fn test_quality_error_display() {
let err = QualityError::InvalidSimplexArity {
actual: 2,
expected: 3,
dimension: 2,
};
assert!(format!("{err}").contains("expected 3"));
let err = QualityError::DegenerateSimplex {
measure: QualityDegeneracyMeasure::Volume,
observed: CoordinateConversionValue::from_f64(0.0),
epsilon: CoordinateConversionValue::from_f64(1e-12),
avg_edge_length: Some(CoordinateConversionValue::from_f64(1.0)),
};
assert!(format!("{err}").contains("Degenerate"));
assert!(format!("{err}").contains("volume observed"));
assert!(format!("{err}").contains("0.0"));
let err = QualityError::NumericConversion {
operation: QualityNumericOperation::EdgeCountConversion,
};
assert!(format!("{err}").contains("Numeric conversion failed"));
assert!(format!("{err}").contains("edge count conversion"));
}
#[test]
fn quality_diagnostic_enums_display_without_debug_variant_names() {
assert_eq!(
QualityNumericOperation::EpsilonFloorConversion.to_string(),
"epsilon floor conversion"
);
assert_eq!(
QualityNumericOperation::EdgeCountConversion.to_string(),
"edge count conversion"
);
assert_eq!(
QualityNumericOperation::RelativeFactorConversion.to_string(),
"relative factor conversion"
);
assert_eq!(
QualityNumericOperation::Circumradius.to_string(),
"circumradius computation"
);
assert_eq!(
QualityNumericOperation::Inradius.to_string(),
"inradius computation"
);
assert_eq!(
QualityNumericOperation::Volume.to_string(),
"simplex volume computation"
);
assert_eq!(QualityDegeneracyMeasure::Inradius.to_string(), "inradius");
assert_eq!(QualityDegeneracyMeasure::Volume.to_string(), "volume");
assert_eq!(
QualityDegeneracyMeasure::AverageEdgeLength.to_string(),
"average edge length"
);
assert_eq!(
QualityDegeneracyMeasure::EdgeLengthPower.to_string(),
"edge length power"
);
}
#[test]
fn test_quality_metrics_correlation() {
let vertices_good = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025]).unwrap(),
];
let dt_good: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices_good)
.build()
.unwrap();
let simplex_key_good = dt_good.simplices().next().unwrap().0;
let vertices_poor = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.5, 0.01]).unwrap(), ];
let dt_poor: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices_poor)
.build()
.unwrap();
let simplex_key_poor = dt_poor.simplices().next().unwrap().0;
let ratio_good = radius_ratio(dt_good.as_triangulation(), simplex_key_good).unwrap();
let ratio_poor = radius_ratio(dt_poor.as_triangulation(), simplex_key_poor).unwrap();
let norm_vol_good =
normalized_volume(dt_good.as_triangulation(), simplex_key_good).unwrap();
let norm_vol_poor =
normalized_volume(dt_poor.as_triangulation(), simplex_key_poor).unwrap();
assert!(ratio_good < ratio_poor);
assert!(norm_vol_good > norm_vol_poor);
}
#[test]
fn test_radius_ratio_perfectly_collinear_3points() {
let vertices = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([2.0, 0.0]).unwrap(), ];
let dt_result: Result<
DelaunayTriangulation<_, (), (), 2>,
DelaunayTriangulationConstructionError,
> = DelaunayTriangulation::builder(&vertices).build();
if let Ok(dt) = dt_result {
let simplex_key = dt.simplices().next().unwrap().0;
let ratio_result = radius_ratio(dt.as_triangulation(), simplex_key);
let vol_result = normalized_volume(dt.as_triangulation(), simplex_key);
assert!(
ratio_result.is_err() || vol_result.is_err(),
"Quality metrics should detect degenerate collinear simplex"
);
}
}
#[test]
fn test_quality_near_duplicate_vertices() {
let vertices = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([1.000_000_1, 0.000_000_1]).unwrap(), ];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
if let Ok(ratio) = radius_ratio(dt.as_triangulation(), simplex_key) {
assert!(ratio > 100.0); }
}
#[test]
fn test_quality_mixed_scale_coordinates() {
let vertices = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1e10, 0.0]).unwrap(),
vertex!([1e-10, 1e10]).unwrap(),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
let ratio = radius_ratio(dt.as_triangulation(), simplex_key)
.expect("mixed-scale simplex should have a radius ratio");
let normalized = normalized_volume(dt.as_triangulation(), simplex_key)
.expect("mixed-scale simplex should have a normalized volume");
assert!(ratio.is_finite() && ratio > 0.0);
assert!(normalized.is_finite() && normalized > 0.0);
}
#[test]
fn test_quality_6d_simplex() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 1.0, 0.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 1.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 0.0, 1.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 0.0, 0.0, 1.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 0.0, 0.0, 0.0, 1.0]).unwrap(),
];
let dt: DelaunayTriangulation<_, (), (), 6> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
let ratio = radius_ratio(dt.as_triangulation(), simplex_key).unwrap();
assert!(ratio > 6.0); assert!(ratio < 20.0);
let norm_vol = normalized_volume(dt.as_triangulation(), simplex_key).unwrap();
assert!(norm_vol > 0.0);
}
macro_rules! test_poor_quality {
($(
$test_name:ident => $dim:expr => $desc:expr =>
$vertices:expr,
$min_ratio:expr
),+ $(,)?) => {
$(
#[test]
fn $test_name() {
let vertices = $vertices;
let dt: DelaunayTriangulation<_, (), (), $dim> = DelaunayTriangulation::builder(&vertices).build().unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
if let Ok(ratio) = radius_ratio(dt.as_triangulation(), simplex_key) {
assert!(ratio > $min_ratio, "{}: ratio={ratio}, expected > {}", $desc, $min_ratio);
}
}
)+
};
}
test_poor_quality! {
poor_quality_2d_flat => 2 => "very flat triangle" =>
vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([100.0, 0.0]).unwrap(),
vertex!([50.0, 0.1]).unwrap(),
],
50.0,
poor_quality_3d_nearly_coplanar => 3 => "nearly coplanar tetrahedron" =>
vec![
vertex!([0.0, 0.0, 0.0]).unwrap(),
vertex!([10.0, 0.0, 0.0]).unwrap(),
vertex!([5.0, 8.66, 0.0]).unwrap(),
vertex!([5.0, 2.89, 0.01]).unwrap(),
],
30.0,
poor_quality_4d_degenerate => 4 => "nearly 3D subspace" =>
vec![
vertex!([0.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0, 0.0]).unwrap(),
vertex!([0.5, 0.866, 0.0, 0.0]).unwrap(),
vertex!([0.5, 0.289, 0.816, 0.0]).unwrap(),
vertex!([0.5, 0.289, 0.204, 0.001]).unwrap(),
],
10.0,
poor_quality_3d_sliver => 3 => "sliver tetrahedron" =>
vec![
vertex!([0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025, 0.0]).unwrap(),
vertex!([0.5, 0.288_675, 0.001]).unwrap(),
],
100.0,
poor_quality_2d_needle => 2 => "needle triangle" =>
vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([100.0, 0.0]).unwrap(),
vertex!([0.0, 0.1]).unwrap(),
],
50.0,
poor_quality_3d_cap => 3 => "cap tetrahedron" =>
vec![
vertex!([0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025, 0.0]).unwrap(),
vertex!([0.5, 0.288_675, 10.0]).unwrap(),
],
10.0,
}
#[test]
fn test_quality_invalid_simplex_key() {
let vertices = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025]).unwrap(),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let invalid_key = SimplexKey::from(KeyData::from_ffi(u64::MAX));
let result = radius_ratio(dt.as_triangulation(), invalid_key);
assert_matches!(result, Err(QualityError::SimplexVertices { .. }));
let result = normalized_volume(dt.as_triangulation(), invalid_key);
assert_matches!(result, Err(QualityError::SimplexVertices { .. }));
}
#[test]
fn test_quality_error_clone_eq() {
let err1 = QualityError::InvalidSimplexArity {
actual: 2,
expected: 3,
dimension: 2,
};
let err2 = err1.clone();
assert_eq!(err1, err2);
let err3 = QualityError::DegenerateSimplex {
measure: QualityDegeneracyMeasure::Volume,
observed: CoordinateConversionValue::from_f64(0.0),
epsilon: CoordinateConversionValue::from_f64(1e-12),
avg_edge_length: None,
};
let err4 = err3.clone();
assert_eq!(err3, err4);
let err5 = QualityError::NumericConversion {
operation: QualityNumericOperation::EdgeCountConversion,
};
let err6 = err5.clone();
assert_eq!(err5, err6);
assert_ne!(err1, err3);
assert_ne!(err3, err5);
}
#[test]
fn test_quality_ranking_and_thresholds() {
let vertices_best = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025]).unwrap(),
];
let dt_best: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices_best)
.build()
.unwrap();
let key_best = dt_best.simplices().next().unwrap().0;
let vertices_medium = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([3.0, 0.0]).unwrap(),
vertex!([0.0, 4.0]).unwrap(),
];
let dt_medium: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices_medium)
.build()
.unwrap();
let key_medium = dt_medium.simplices().next().unwrap().0;
let vertices_worst = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([10.0, 0.0]).unwrap(),
vertex!([5.0, 0.1]).unwrap(),
];
let dt_worst: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices_worst)
.build()
.unwrap();
let key_worst = dt_worst.simplices().next().unwrap().0;
let ratio_best = radius_ratio(dt_best.as_triangulation(), key_best).unwrap();
let ratio_medium = radius_ratio(dt_medium.as_triangulation(), key_medium).unwrap();
let ratio_worst = radius_ratio(dt_worst.as_triangulation(), key_worst).unwrap();
let vol_best = normalized_volume(dt_best.as_triangulation(), key_best).unwrap();
let vol_medium = normalized_volume(dt_medium.as_triangulation(), key_medium).unwrap();
let vol_worst = normalized_volume(dt_worst.as_triangulation(), key_worst).unwrap();
assert!(ratio_best < ratio_medium);
assert!(ratio_medium < ratio_worst);
assert!(vol_best > vol_medium);
assert!(vol_medium > vol_worst);
assert!(ratio_best < 4.0, "Good quality: ratio < 4");
assert!(ratio_medium < 10.0, "Acceptable quality: ratio < 10");
assert!(ratio_worst >= 10.0, "Poor quality: ratio >= 10");
}
#[test]
fn test_special_triangles() {
let vertices_right = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.0, 1.0]).unwrap(),
];
let dt_right: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices_right)
.build()
.unwrap();
let key_right = dt_right.simplices().next().unwrap().0;
let ratio_right = radius_ratio(dt_right.as_triangulation(), key_right).unwrap();
assert_relative_eq!(ratio_right, 1.0 + 2.0_f64.sqrt(), epsilon = 0.1);
let vertices_iso = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([2.0, 0.0]).unwrap(),
vertex!([1.0, 2.0]).unwrap(),
];
let dt_iso: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices_iso)
.build()
.unwrap();
let key_iso = dt_iso.simplices().next().unwrap().0;
let ratio_iso = radius_ratio(dt_iso.as_triangulation(), key_iso).unwrap();
assert!(ratio_iso > 2.0 && ratio_iso < 5.0);
}
#[test]
fn test_simplex_points_valid() {
let vertices = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025]).unwrap(),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
let points = simplex_points(dt.as_triangulation(), simplex_key).unwrap();
assert_eq!(points.len(), 3, "Should have 3 points for 2D simplex");
}
#[test]
fn test_simplex_points_invalid_key() {
let vertices = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025]).unwrap(),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let invalid_key = SimplexKey::from(KeyData::from_ffi(u64::MAX));
let result = simplex_points(dt.as_triangulation(), invalid_key);
assert_matches!(result, Err(QualityError::SimplexVertices { .. }));
}
#[test]
fn test_scale_aware_epsilon_2d() {
let mut points = SmallBuffer::new();
points.push(Point::try_new([0.0, 0.0]).expect("finite point coordinates"));
points.push(Point::try_new([1.0, 0.0]).expect("finite point coordinates"));
points.push(Point::try_new([0.5, 0.866_025]).expect("finite point coordinates"));
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points);
assert!(
avg_edge_length > 0.0,
"Average edge length should be positive"
);
assert!(epsilon > 0.0, "Epsilon should be positive");
assert!(epsilon >= 1e-12, "Epsilon should have floor of 1e-12");
}
#[test]
fn test_scale_aware_epsilon_tiny_simplex() {
let mut points = SmallBuffer::new();
points.push(Point::try_new([0.0, 0.0]).expect("finite point coordinates"));
points.push(Point::try_new([1e-10, 0.0]).expect("finite point coordinates"));
points.push(Point::try_new([0.5e-10, 0.866_025e-10]).expect("finite point coordinates"));
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points);
assert!(epsilon >= 1e-12);
assert!(avg_edge_length > 0.0);
}
#[test]
fn test_scale_aware_epsilon_large_simplex() {
let mut points = SmallBuffer::new();
points.push(Point::try_new([0.0, 0.0]).expect("finite point coordinates"));
points.push(Point::try_new([1e6, 0.0]).expect("finite point coordinates"));
points.push(Point::try_new([0.5e6, 0.866_025e6]).expect("finite point coordinates"));
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points);
assert!(epsilon > 1e-12);
assert!(avg_edge_length > 1e5);
}
#[test]
fn test_scale_aware_epsilon_3d() {
let mut points = SmallBuffer::new();
points.push(Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"));
points.push(Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"));
points.push(Point::try_new([0.0, 1.0, 0.0]).expect("finite point coordinates"));
points.push(Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"));
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points);
assert!(avg_edge_length > 0.0);
assert!(epsilon > 0.0);
}
#[test]
fn test_radius_ratio_wrong_vertex_count() {
let vertices = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.5, 0.866_025]).unwrap(),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
let result = radius_ratio(dt.as_triangulation(), simplex_key);
assert!(result.is_ok());
}
#[test]
fn test_normalized_volume_wrong_vertex_count() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 1.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 1.0]).unwrap(),
];
let dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
let result = normalized_volume(dt.as_triangulation(), simplex_key);
assert!(result.is_ok());
}
#[test]
fn test_radius_ratio_numerical_edge_cases() {
let vertices = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([0.5, 1e-15]).unwrap(), ];
let dt_result: Result<
DelaunayTriangulation<_, (), (), 2>,
DelaunayTriangulationConstructionError,
> = DelaunayTriangulation::builder(&vertices).build();
match dt_result {
Ok(dt) => {
let simplex_key = dt.simplices().next().unwrap().0;
let result = radius_ratio(dt.as_triangulation(), simplex_key);
assert!(
result.is_ok()
|| matches!(result, Err(QualityError::DegenerateSimplex { .. }))
|| matches!(result, Err(QualityError::NumericConversion { .. }))
|| matches!(result, Err(QualityError::Circumradius { .. }))
|| matches!(result, Err(QualityError::Inradius { .. }))
);
}
Err(DelaunayTriangulationConstructionError::Triangulation(
DelaunayConstructionFailure::GeometricDegeneracy { .. },
)) => {
}
Err(other) => panic!("Unexpected triangulation error for numerical edge case: {other}"),
}
}
#[test]
fn test_normalized_volume_numerical_edge_cases() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 1.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 1e-14]).unwrap(), ];
let dt_result: Result<
DelaunayTriangulation<_, (), (), 3>,
DelaunayTriangulationConstructionError,
> = DelaunayTriangulation::builder(&vertices).build();
match dt_result {
Ok(dt) => {
let simplex_key = dt.simplices().next().unwrap().0;
let result = normalized_volume(dt.as_triangulation(), simplex_key);
assert!(
result.is_ok()
|| matches!(result, Err(QualityError::DegenerateSimplex { .. }))
|| matches!(result, Err(QualityError::NumericConversion { .. }))
|| matches!(result, Err(QualityError::Volume { .. }))
);
}
Err(DelaunayTriangulationConstructionError::Triangulation(
DelaunayConstructionFailure::GeometricDegeneracy { .. }
| DelaunayConstructionFailure::InsufficientVertices { .. },
)) => {
}
Err(other) => panic!(
"Unexpected triangulation error for normalized_volume numerical edge case: {other}",
),
}
}
#[test]
fn normalized_volume_uses_dimensionally_consistent_volume_threshold() {
let mut tds: Tds<(), (), 2> = Tds::empty();
let v0 = tds
.insert_vertex_with_mapping(vertex!([0.0, 0.0]).unwrap())
.unwrap();
let v1 = tds
.insert_vertex_with_mapping(vertex!([1.0, 0.0]).unwrap())
.unwrap();
let v2 = tds
.insert_vertex_with_mapping(vertex!([1.0, 1.0e-30]).unwrap())
.unwrap();
let simplex_key = tds
.insert_simplex_with_mapping(
Simplex::try_new_with_data(vec![v0, v1, v2], None).unwrap(),
)
.unwrap();
let tri = Triangulation::<FastKernel<f64>, (), (), 2>::new_with_tds(FastKernel::new(), tds);
let err = normalized_volume(&tri, simplex_key).unwrap_err();
let QualityError::DegenerateSimplex {
measure,
epsilon,
avg_edge_length,
..
} = err
else {
panic!("expected volume degeneracy, got {err}");
};
assert_eq!(measure, QualityDegeneracyMeasure::Volume);
let epsilon = scalar_payload(&epsilon);
let avg_edge_length = scalar_payload(&avg_edge_length.unwrap());
let linear_epsilon = 1.0e-12_f64.max(avg_edge_length * 1.0e-8);
let expected_area_epsilon = linear_epsilon * linear_epsilon;
assert!(
(epsilon - expected_area_epsilon).abs() <= expected_area_epsilon * 1.0e-12,
"volume threshold should be epsilon^D; got {epsilon}, expected {expected_area_epsilon}"
);
}
#[test]
fn test_quality_error_source_trait() {
let err = QualityError::NumericConversion {
operation: QualityNumericOperation::EdgeCountConversion,
};
assert!(std::error::Error::source(&err).is_none());
assert!(format!("{err}").contains("Numeric conversion failed"));
}
#[test]
fn test_degenerate_simplex_error_details() {
let vertices = vec![
vertex!([0.0, 0.0]).unwrap(),
vertex!([1.0, 0.0]).unwrap(),
vertex!([2.0, 1e-20]).unwrap(), ];
let dt_result: Result<
DelaunayTriangulation<_, (), (), 2>,
DelaunayTriangulationConstructionError,
> = DelaunayTriangulation::builder(&vertices).build();
match dt_result {
Ok(dt) => {
let simplex_key = dt.simplices().next().unwrap().0;
let result = radius_ratio(dt.as_triangulation(), simplex_key);
if let Err(QualityError::DegenerateSimplex {
measure, observed, ..
}) = result
{
assert_matches!(
measure,
QualityDegeneracyMeasure::Inradius
| QualityDegeneracyMeasure::Volume
| QualityDegeneracyMeasure::AverageEdgeLength
| QualityDegeneracyMeasure::EdgeLengthPower
);
assert_matches!(observed, CoordinateConversionValue::Scalar(_));
}
}
Err(DelaunayTriangulationConstructionError::Triangulation(
DelaunayConstructionFailure::GeometricDegeneracy { .. },
)) => {
}
Err(other) => {
panic!("Unexpected triangulation error for degenerate simplex test: {other}")
}
}
}
}