#![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::CoordinateScalar,
util::{CircumcenterError, circumradius, hypot, inradius as simplex_inradius, simplex_volume},
};
use num_traits::{NumCast, One};
use std::ops::{AddAssign, Div};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum QualityNumericOperation {
EpsilonFloorConversion,
EdgeCountConversion,
RelativeFactorConversion,
Circumradius,
Inradius,
Volume,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum QualityDegeneracyMeasure {
Inradius,
Volume,
AverageEdgeLength,
EdgeLengthPower,
}
#[derive(Debug, Clone, PartialEq, Eq, 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("{message}")]
UnexpectedTdsFailure {
message: String,
},
}
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 {
message: other.to_string(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, 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: CircumcenterError,
},
#[error("Inradius computation failed: {source}")]
Inradius {
#[source]
source: CircumcenterError,
},
#[error("Volume computation failed: {source}")]
Volume {
#[source]
source: CircumcenterError,
},
#[error(
"Degenerate simplex: {measure:?} observed={observed}, epsilon={epsilon}, avg_edge_length={avg_edge_length:?}"
)]
DegenerateSimplex {
measure: QualityDegeneracyMeasure,
observed: String,
epsilon: String,
avg_edge_length: Option<String>,
},
}
fn simplex_points<K, U, V, const D: usize>(
tri: &Triangulation<K, U, V, D>,
simplex_key: SimplexKey,
) -> Result<SmallBuffer<Point<K::Scalar, D>, MAX_PRACTICAL_DIMENSION_SIZE>, QualityError>
where
K: Kernel<D>,
{
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<T, const D: usize>(
points: &SmallBuffer<Point<T, D>, MAX_PRACTICAL_DIMENSION_SIZE>,
) -> Result<(T, T), QualityError>
where
T: CoordinateScalar + AddAssign<T>,
{
let mut total_edge_length = T::zero();
let mut edge_count = 0;
for i in 0..points.len() {
for j in (i + 1)..points.len() {
let mut diff_coords = [T::zero(); D];
for (idx, diff) in diff_coords.iter_mut().enumerate() {
*diff = points[i].coords()[idx] - points[j].coords()[idx];
}
let dist = hypot(&diff_coords);
total_edge_length += dist;
edge_count += 1;
}
}
if edge_count == 0 {
let floor: T = NumCast::from(1e-12).ok_or(QualityError::NumericConversion {
operation: QualityNumericOperation::EpsilonFloorConversion,
})?;
return Ok((T::zero(), floor));
}
let edge_count_t = NumCast::from(edge_count).ok_or(QualityError::NumericConversion {
operation: QualityNumericOperation::EdgeCountConversion,
})?;
let avg_edge_length = total_edge_length / edge_count_t;
let floor: T = NumCast::from(1e-12).ok_or(QualityError::NumericConversion {
operation: QualityNumericOperation::EpsilonFloorConversion,
})?;
let relative_factor: T = NumCast::from(1e-8).ok_or(QualityError::NumericConversion {
operation: QualityNumericOperation::RelativeFactorConversion,
})?;
let epsilon = floor.max(avg_edge_length * relative_factor);
Ok((avg_edge_length, epsilon))
}
pub fn radius_ratio<K, U, V, const D: usize>(
tri: &Triangulation<K, U, V, D>,
simplex_key: SimplexKey,
) -> Result<K::Scalar, QualityError>
where
K: Kernel<D>,
K::Scalar: Div<Output = K::Scalar>,
{
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 })?;
let inradius_val =
simplex_inradius(&points).map_err(|source| QualityError::Inradius { source })?;
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points)?;
if inradius_val < epsilon {
return Err(QualityError::DegenerateSimplex {
measure: QualityDegeneracyMeasure::Inradius,
observed: format!("{inradius_val:?}"),
epsilon: format!("{epsilon:?}"),
avg_edge_length: Some(format!("{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<K::Scalar, QualityError>
where
K: Kernel<D>,
K::Scalar: Div<Output = K::Scalar>,
{
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 })?;
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points)?;
let mut epsilon_pow = K::Scalar::one();
for _ in 0..D {
epsilon_pow = epsilon_pow * epsilon;
}
if volume < epsilon_pow {
return Err(QualityError::DegenerateSimplex {
measure: QualityDegeneracyMeasure::Volume,
observed: format!("{volume:?}"),
epsilon: format!("{epsilon_pow:?}"),
avg_edge_length: Some(format!("{avg_edge_length:?}")),
});
}
if avg_edge_length < epsilon {
return Err(QualityError::DegenerateSimplex {
measure: QualityDegeneracyMeasure::AverageEdgeLength,
observed: format!("{avg_edge_length:?}"),
epsilon: format!("{epsilon:?}"),
avg_edge_length: Some(format!("{avg_edge_length:?}")),
});
}
let mut edge_length_power = K::Scalar::one();
for _ in 0..D {
edge_length_power = edge_length_power * avg_edge_length;
}
if edge_length_power < epsilon_pow {
return Err(QualityError::DegenerateSimplex {
measure: QualityDegeneracyMeasure::EdgeLengthPower,
observed: format!("{edge_length_power:?}"),
epsilon: format!("{epsilon_pow:?}"),
avg_edge_length: Some(format!("{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::{AdaptiveKernel, FastKernel};
use crate::geometry::traits::coordinate::Coordinate;
use crate::triangulation::DelaunayTriangulation;
use crate::vertex;
use approx::assert_relative_eq;
use slotmap::KeyData;
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::new(&vertices).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::new(&vertices_base).unwrap();
let key_base = dt_base.simplices().next().unwrap().0;
let vertices_scaled: Vec<_> = vertices_base.iter().map(|v| {
let coords: [f64; $dim] = std::array::from_fn(|idx| v.point().coords()[idx] * 10.0);
vertex!(coords)
}).collect();
let dt_scaled: DelaunayTriangulation<_, (), (), $dim> = DelaunayTriangulation::new(&vertices_scaled).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::new(&vertices_base).unwrap();
let key_base = dt_base.simplices().next().unwrap().0;
let vertices_translated: Vec<_> = vertices_base.iter().map(|v| {
let coords: [f64; $dim] = std::array::from_fn(|idx| v.point().coords()[idx] + 5.0);
vertex!(coords)
}).collect();
let dt_translated: DelaunayTriangulation<_, (), (), $dim> = DelaunayTriangulation::new(&vertices_translated).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]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
],
2.0, 3.0,
quality_2d_equilateral => 2 => "equilateral triangle" =>
vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.5, 0.866_025]),
],
1.9, 2.1,
quality_2d_right => 2 => "right triangle" =>
vec![
vertex!([0.0, 0.0]),
vertex!([3.0, 0.0]),
vertex!([0.0, 4.0]),
],
2.0, 5.0,
quality_3d_unit => 3 => "unit simplex" =>
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]),
],
3.0, 5.0,
quality_3d_regular => 3 => "regular tetrahedron" =>
vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.5, 0.866_025, 0.0]),
vertex!([0.5, 0.288_675, 0.816_497]),
],
2.8, 3.2,
quality_4d_unit => 4 => "unit simplex" =>
vec![
vertex!([0.0, 0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0, 0.0]),
vertex!([0.0, 0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 0.0, 1.0]),
],
4.0, 7.0,
quality_4d_regular => 4 => "regular simplex" =>
vec![
vertex!([0.0, 0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0, 0.0]),
vertex!([0.5, 0.866_025, 0.0, 0.0]),
vertex!([0.5, 0.288_675, 0.816_497, 0.0]),
vertex!([0.5, 0.288_675, 0.204_124, 0.790_569]),
],
3.8, 4.2,
quality_5d_unit => 5 => "unit simplex" =>
vec![
vertex!([0.0, 0.0, 0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0, 0.0, 0.0]),
vertex!([0.0, 0.0, 1.0, 0.0, 0.0]),
vertex!([0.0, 0.0, 0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 0.0, 0.0, 1.0]),
],
5.0, 15.0,
}
#[test]
fn test_degenerate_nearly_collinear() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([2.0, 0.001]),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices).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 { message }
if message.contains("Duplicate simplices")
&& message.contains("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::<f64, 0>::new([]));
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points).unwrap();
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: "0.0".to_string(),
epsilon: "1e-12".to_string(),
avg_edge_length: Some("1.0".to_string()),
};
assert!(format!("{err}").contains("Degenerate"));
assert!(format!("{err}").contains("0.0"));
let err = QualityError::NumericConversion {
operation: QualityNumericOperation::EdgeCountConversion,
};
assert!(format!("{err}").contains("Numeric conversion failed"));
}
#[test]
fn test_quality_metrics_correlation() {
let vertices_good = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.5, 0.866_025]),
];
let dt_good: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices_good).unwrap();
let simplex_key_good = dt_good.simplices().next().unwrap().0;
let vertices_poor = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.5, 0.01]), ];
let dt_poor: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices_poor).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_quality_metrics_f32_compatibility() {
let vertices = vec![
vertex!([0.0f32, 0.0f32]),
vertex!([1.0f32, 0.0f32]),
vertex!([0.5f32, 0.866f32]), ];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices).unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
let ratio = radius_ratio(dt.as_triangulation(), simplex_key).unwrap();
assert!(ratio > 1.5 && ratio < 2.5, "ratio={ratio}");
let norm_vol = normalized_volume(dt.as_triangulation(), simplex_key).unwrap();
assert!(norm_vol > 0.3 && norm_vol < 0.6, "norm_vol={norm_vol}");
}
#[test]
fn test_radius_ratio_perfectly_collinear_3points() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([2.0, 0.0]), ];
let dt_result: Result<
DelaunayTriangulation<_, (), (), 2>,
DelaunayTriangulationConstructionError,
> = DelaunayTriangulation::new(&vertices);
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]),
vertex!([1.0, 0.0]),
vertex!([1.000_000_1, 0.000_000_1]), ];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices).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]),
vertex!([1e10, 0.0]),
vertex!([1e-10, 1e10]),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices).unwrap();
let simplex_key = dt.simplices().next().unwrap().0;
let ratio_result = radius_ratio(dt.as_triangulation(), simplex_key);
let norm_vol_result = normalized_volume(dt.as_triangulation(), simplex_key);
assert!(ratio_result.is_ok() || ratio_result.is_err());
assert!(norm_vol_result.is_ok() || norm_vol_result.is_err());
}
#[test]
fn test_quality_6d_simplex() {
let vertices = vec![
vertex!([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0, 0.0, 0.0, 0.0]),
vertex!([0.0, 0.0, 1.0, 0.0, 0.0, 0.0]),
vertex!([0.0, 0.0, 0.0, 1.0, 0.0, 0.0]),
vertex!([0.0, 0.0, 0.0, 0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 0.0, 0.0, 0.0, 1.0]),
];
let dt: DelaunayTriangulation<_, (), (), 6> =
DelaunayTriangulation::new(&vertices).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::new(&vertices).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]),
vertex!([100.0, 0.0]),
vertex!([50.0, 0.1]),
],
50.0,
poor_quality_3d_nearly_coplanar => 3 => "nearly coplanar tetrahedron" =>
vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([10.0, 0.0, 0.0]),
vertex!([5.0, 8.66, 0.0]),
vertex!([5.0, 2.89, 0.01]),
],
30.0,
poor_quality_4d_degenerate => 4 => "nearly 3D subspace" =>
vec![
vertex!([0.0, 0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0, 0.0]),
vertex!([0.5, 0.866, 0.0, 0.0]),
vertex!([0.5, 0.289, 0.816, 0.0]),
vertex!([0.5, 0.289, 0.204, 0.001]),
],
10.0,
poor_quality_3d_sliver => 3 => "sliver tetrahedron" =>
vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.5, 0.866_025, 0.0]),
vertex!([0.5, 0.288_675, 0.001]),
],
100.0,
poor_quality_2d_needle => 2 => "needle triangle" =>
vec![
vertex!([0.0, 0.0]),
vertex!([100.0, 0.0]),
vertex!([0.0, 0.1]),
],
50.0,
poor_quality_3d_cap => 3 => "cap tetrahedron" =>
vec![
vertex!([0.0, 0.0, 0.0]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.5, 0.866_025, 0.0]),
vertex!([0.5, 0.288_675, 10.0]),
],
10.0,
}
#[test]
fn test_quality_invalid_simplex_key() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.5, 0.866_025]),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices).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: "0".to_string(),
epsilon: "1e-12".to_string(),
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]),
vertex!([1.0, 0.0]),
vertex!([0.5, 0.866_025]),
];
let dt_best: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices_best).unwrap();
let key_best = dt_best.simplices().next().unwrap().0;
let vertices_medium = vec![
vertex!([0.0, 0.0]),
vertex!([3.0, 0.0]),
vertex!([0.0, 4.0]),
];
let dt_medium: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices_medium).unwrap();
let key_medium = dt_medium.simplices().next().unwrap().0;
let vertices_worst = vec![
vertex!([0.0, 0.0]),
vertex!([10.0, 0.0]),
vertex!([5.0, 0.1]),
];
let dt_worst: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices_worst).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]),
vertex!([1.0, 0.0]),
vertex!([0.0, 1.0]),
];
let dt_right: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices_right).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]),
vertex!([2.0, 0.0]),
vertex!([1.0, 2.0]),
];
let dt_iso: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices_iso).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_quality_precision_f32_vs_f64() {
let vertices_f32 = vec![
vertex!([0.0f32, 0.0f32]),
vertex!([1.0f32, 0.0f32]),
vertex!([0.5f32, 0.866f32]),
];
let dt_f32: DelaunayTriangulation<AdaptiveKernel<f32>, (), (), 2> =
DelaunayTriangulation::with_kernel(&AdaptiveKernel::new(), &vertices_f32).unwrap();
let key_f32 = dt_f32.simplices().next().unwrap().0;
let vertices_f64 = vec![
vertex!([0.0f64, 0.0f64]),
vertex!([1.0f64, 0.0f64]),
vertex!([0.5f64, 0.866_025f64]),
];
let dt_f64: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices_f64).unwrap();
let key_f64 = dt_f64.simplices().next().unwrap().0;
let ratio_f32 = radius_ratio(dt_f32.as_triangulation(), key_f32).unwrap();
let ratio_f64 = radius_ratio(dt_f64.as_triangulation(), key_f64).unwrap();
let ratio_diff = (<f64 as std::convert::From<f32>>::from(ratio_f32) - ratio_f64).abs();
assert!(
ratio_diff < 0.1,
"f32/f64 precision difference too large: {ratio_diff}"
);
}
#[test]
fn test_simplex_points_valid() {
let vertices = vec![
vertex!([0.0, 0.0]),
vertex!([1.0, 0.0]),
vertex!([0.5, 0.866_025]),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices).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]),
vertex!([1.0, 0.0]),
vertex!([0.5, 0.866_025]),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices).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::new([0.0, 0.0]));
points.push(Point::new([1.0, 0.0]));
points.push(Point::new([0.5, 0.866_025]));
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points).unwrap();
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::new([0.0, 0.0]));
points.push(Point::new([1e-10, 0.0]));
points.push(Point::new([0.5e-10, 0.866_025e-10]));
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points).unwrap();
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::new([0.0, 0.0]));
points.push(Point::new([1e6, 0.0]));
points.push(Point::new([0.5e6, 0.866_025e6]));
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points).unwrap();
assert!(epsilon > 1e-12);
assert!(avg_edge_length > 1e5);
}
#[test]
fn test_scale_aware_epsilon_3d() {
let mut points = SmallBuffer::new();
points.push(Point::new([0.0, 0.0, 0.0]));
points.push(Point::new([1.0, 0.0, 0.0]));
points.push(Point::new([0.0, 1.0, 0.0]));
points.push(Point::new([0.0, 0.0, 1.0]));
let (avg_edge_length, epsilon) = scale_aware_epsilon(&points).unwrap();
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]),
vertex!([1.0, 0.0]),
vertex!([0.5, 0.866_025]),
];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::new(&vertices).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]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1.0]),
];
let dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::new(&vertices).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]),
vertex!([1.0, 0.0]),
vertex!([0.5, 1e-15]), ];
let dt_result: Result<
DelaunayTriangulation<_, (), (), 2>,
DelaunayTriangulationConstructionError,
> = DelaunayTriangulation::new(&vertices);
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]),
vertex!([1.0, 0.0, 0.0]),
vertex!([0.0, 1.0, 0.0]),
vertex!([0.0, 0.0, 1e-14]), ];
let dt_result: Result<
DelaunayTriangulation<_, (), (), 3>,
DelaunayTriangulationConstructionError,
> = DelaunayTriangulation::new(&vertices);
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<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!([1.0, 1.0e-30]))
.unwrap();
let simplex_key = tds
.insert_simplex_with_mapping(Simplex::new(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: f64 = epsilon.parse().unwrap();
let avg_edge_length: f64 = avg_edge_length.unwrap().parse().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]),
vertex!([1.0, 0.0]),
vertex!([2.0, 1e-20]), ];
let dt_result: Result<
DelaunayTriangulation<_, (), (), 2>,
DelaunayTriangulationConstructionError,
> = DelaunayTriangulation::new(&vertices);
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!(!observed.is_empty());
}
}
Err(DelaunayTriangulationConstructionError::Triangulation(
DelaunayConstructionFailure::GeometricDegeneracy { .. },
)) => {
}
Err(other) => {
panic!("Unexpected triangulation error for degenerate simplex test: {other}")
}
}
}
}