#![forbid(unsafe_code)]
use super::circumsphere::{
CircumcenterError, CircumcenterFailureReason, DegenerateGeometry, DegenerateMeasure,
};
use super::conversions::{ValueConversionError, safe_coords_to_f64, safe_usize_to_scalar};
use super::norms::hypot;
use crate::core::facet::FacetView;
use crate::geometry::matrix::{DEFAULT_SINGULAR_TOL, Matrix, matrix_get, matrix_set};
use crate::geometry::point::Point;
use crate::geometry::traits::coordinate::CoordinateConversionValue;
use crate::tds::FacetError;
use num_traits::Float;
#[derive(Clone, Debug, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum SurfaceMeasureError {
#[error("Failed to retrieve facet vertices: {source}")]
FacetVertices {
#[from]
source: FacetError,
},
#[error("Geometry computation failed: {source}")]
GeometryError {
#[source]
source: Box<CircumcenterError>,
},
}
impl From<CircumcenterError> for SurfaceMeasureError {
fn from(source: CircumcenterError) -> Self {
Self::GeometryError {
source: Box::new(source),
}
}
}
const DEGENERACY_EPSILON_FACTOR: f64 = 64.0;
fn degeneracy_tolerance(scale: f64) -> f64 {
if scale <= 0.0 {
return 0.0;
}
scale * DEGENERACY_EPSILON_FACTOR * f64::EPSILON
}
fn is_zero_or_roundoff(value: f64, scale: f64) -> bool {
let magnitude = Float::abs(value);
magnitude == 0.0 || magnitude <= degeneracy_tolerance(scale)
}
fn ensure_finite_measure_value(
value: f64,
measure: DegenerateMeasure,
) -> Result<(), CircumcenterError> {
if value.is_finite() {
Ok(())
} else {
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NonFiniteMeasure {
measure,
value: CoordinateConversionValue::from_numeric_debug(&value),
},
})
}
}
fn ensure_finite_measure_values<const N: usize>(
values: &[f64; N],
measure: DegenerateMeasure,
) -> Result<(), CircumcenterError> {
for value in values {
ensure_finite_measure_value(*value, measure)?;
}
Ok(())
}
pub fn simplex_volume<const D: usize>(points: &[Point<D>]) -> Result<f64, CircumcenterError> {
#[cfg(debug_assertions)]
if std::env::var_os("DELAUNAY_DEBUG_UNUSED_IMPORTS").is_some() {
tracing::debug!(
points_len = points.len(),
dimension = D,
"measures::simplex_volume called"
);
}
if points.len() != D + 1 {
return Err(CircumcenterError::InvalidSimplex {
actual: points.len(),
expected: D + 1,
dimension: D,
});
}
match D {
1 => {
let p0 = points[0].coords();
let p1 = points[1].coords();
let diff = [p1[0] - p0[0]];
ensure_finite_measure_values(&diff, DegenerateMeasure::Length)?;
let length = Float::abs(diff[0]);
ensure_finite_measure_value(length, DegenerateMeasure::Length)?;
if length == 0.0 {
return Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateSimplex {
measure: DegenerateMeasure::Length,
degeneracy: DegenerateGeometry::CoincidentPoints,
},
});
}
Ok(length)
}
2 => {
let p0 = points[0].coords();
let p1 = points[1].coords();
let p2 = points[2].coords();
let v1 = [p1[0] - p0[0], p1[1] - p0[1]];
let v2 = [p2[0] - p0[0], p2[1] - p0[1]];
ensure_finite_measure_values(&v1, DegenerateMeasure::Area)?;
ensure_finite_measure_values(&v2, DegenerateMeasure::Area)?;
let cross_z = v1[1].mul_add(-v2[0], v1[0] * v2[1]);
ensure_finite_measure_value(cross_z, DegenerateMeasure::Area)?;
let area = Float::abs(cross_z) / 2.0;
ensure_finite_measure_value(area, DegenerateMeasure::Area)?;
let cross_scale = Float::abs(v1[0] * v2[1]) + Float::abs(v1[1] * v2[0]);
ensure_finite_measure_value(cross_scale, DegenerateMeasure::Area)?;
if is_zero_or_roundoff(cross_z, cross_scale) {
return Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateSimplex {
measure: DegenerateMeasure::Volume,
degeneracy: DegenerateGeometry::CollinearPoints,
},
});
}
Ok(area)
}
3 => {
let p0 = points[0].coords();
let p1 = points[1].coords();
let p2 = points[2].coords();
let p3 = points[3].coords();
let v1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
let v2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
let v3 = [p3[0] - p0[0], p3[1] - p0[1], p3[2] - p0[2]];
ensure_finite_measure_values(&v1, DegenerateMeasure::Volume)?;
ensure_finite_measure_values(&v2, DegenerateMeasure::Volume)?;
ensure_finite_measure_values(&v3, DegenerateMeasure::Volume)?;
let cross_x = v2[2].mul_add(-v3[1], v2[1] * v3[2]);
let cross_y = v2[0].mul_add(-v3[2], v2[2] * v3[0]);
let cross_z = v2[1].mul_add(-v3[0], v2[0] * v3[1]);
ensure_finite_measure_values(&[cross_x, cross_y, cross_z], DegenerateMeasure::Volume)?;
let triple_product = v1[2].mul_add(cross_z, v1[1].mul_add(cross_y, v1[0] * cross_x));
ensure_finite_measure_value(triple_product, DegenerateMeasure::Volume)?;
let six = 6.0;
let volume = Float::abs(triple_product) / six;
ensure_finite_measure_value(volume, DegenerateMeasure::Volume)?;
let triple_scale = Float::abs(v1[0] * v2[1] * v3[2])
+ Float::abs(v1[0] * v2[2] * v3[1])
+ Float::abs(v1[1] * v2[2] * v3[0])
+ Float::abs(v1[1] * v2[0] * v3[2])
+ Float::abs(v1[2] * v2[0] * v3[1])
+ Float::abs(v1[2] * v2[1] * v3[0]);
ensure_finite_measure_value(triple_scale, DegenerateMeasure::Volume)?;
if is_zero_or_roundoff(triple_product, triple_scale) {
return Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateSimplex {
measure: DegenerateMeasure::Volume,
degeneracy: DegenerateGeometry::CoplanarPoints,
},
});
}
Ok(volume)
}
_ => {
simplex_volume_gram_matrix::<D>(points)
}
}
}
fn validate_gram_determinant(det: f64) -> Result<f64, CircumcenterError> {
if !det.is_finite() {
return Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NonFiniteGramDeterminant,
});
}
if det < 0.0 {
return Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NegativeGramDeterminant,
});
}
if det == 0.0 {
return Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateSimplex {
measure: DegenerateMeasure::Volume,
degeneracy: DegenerateGeometry::CollinearOrCoplanarPoints,
},
});
}
Ok(det)
}
fn factorial_f64(n: usize) -> Result<f64, CircumcenterError> {
let mut value = 1.0f64;
for k in 2..=n {
let k_f64 = safe_usize_to_scalar(k).map_err(|e| CircumcenterError::ValueConversion {
source: Box::new(ValueConversionError::CoordinateConversion {
value: CoordinateConversionValue::from_usize(k),
from_type: "usize",
to_type: "f64",
source: Box::new(e),
}),
})?;
value *= k_f64;
}
Ok(value)
}
#[inline]
fn gram_determinant_ldlt<const D: usize>(gram_matrix: Matrix<D>) -> Result<f64, CircumcenterError> {
let ldlt = gram_matrix.ldlt(DEFAULT_SINGULAR_TOL)?;
ldlt.det().map_err(CircumcenterError::from)
}
fn simplex_volume_gram_matrix<const D: usize>(
points: &[Point<D>],
) -> Result<f64, CircumcenterError> {
let p0_coords = points[0].coords();
let p0_f64 = safe_coords_to_f64(p0_coords)?;
let mut edge_matrix = Matrix::<D>::zero();
for (row, point) in points.iter().skip(1).enumerate() {
let point_f64 = safe_coords_to_f64(point.coords())?;
for (j, (&p, &p0)) in point_f64.iter().zip(p0_f64.iter()).enumerate() {
matrix_set(&mut edge_matrix, row, j, p - p0)?;
}
}
let mut gram_matrix = Matrix::<D>::zero();
for i in 0..D {
for j in 0..D {
let mut dot_product = 0.0;
for k in 0..D {
let edge_i = matrix_get(&edge_matrix, i, k)?;
let edge_j = matrix_get(&edge_matrix, j, k)?;
dot_product = edge_i.mul_add(edge_j, dot_product);
}
matrix_set(&mut gram_matrix, i, j, dot_product)?;
}
}
let det = validate_gram_determinant(gram_determinant_ldlt(gram_matrix)?)?;
let volume_f64 = {
let sqrt_det = det.sqrt();
let d_fact = factorial_f64(D)?;
sqrt_det / d_fact
};
Ok(volume_f64)
}
pub fn inradius<const D: usize>(points: &[Point<D>]) -> Result<f64, CircumcenterError> {
if points.len() != D + 1 {
return Err(CircumcenterError::InvalidSimplex {
actual: points.len(),
expected: D + 1,
dimension: D,
});
}
if D == 1 {
let length = simplex_volume(points)?; return Ok(length / 2.0);
}
let volume = simplex_volume(points)?;
if volume <= 0.0 {
return Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NonPositiveSimplexMeasure {
measure: DegenerateMeasure::Volume,
value: CoordinateConversionValue::from_numeric_debug(&volume),
},
});
}
let mut surface_area = 0.0;
for i in 0..=D {
let facet_points: Vec<Point<D>> = points
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, p)| *p)
.collect();
if facet_points.len() != D {
continue;
}
let facet_area = facet_measure(&facet_points)?;
surface_area += facet_area;
}
if surface_area <= 0.0 {
return Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NonPositiveSimplexMeasure {
measure: DegenerateMeasure::SurfaceArea,
value: CoordinateConversionValue::from_numeric_debug(&surface_area),
},
});
}
let d_scalar = safe_usize_to_scalar(D).map_err(|e| CircumcenterError::ValueConversion {
source: Box::new(ValueConversionError::CoordinateConversion {
value: CoordinateConversionValue::from_usize(D),
from_type: "usize",
to_type: "f64",
source: Box::new(e),
}),
})?;
let inradius = (d_scalar * volume) / surface_area;
Ok(inradius)
}
pub fn facet_measure<const D: usize>(points: &[Point<D>]) -> Result<f64, CircumcenterError> {
if points.len() != D {
return Err(CircumcenterError::InvalidSimplex {
actual: points.len(),
expected: D,
dimension: D,
});
}
match D {
1 => {
if points.len() != 1 {
return Err(CircumcenterError::InvalidSimplex {
actual: points.len(),
expected: 1,
dimension: 1,
});
}
Ok(0.0)
}
2 => {
let p0 = points[0].coords();
let p1 = points[1].coords();
let diff = [p1[0] - p0[0], p1[1] - p0[1]];
ensure_finite_measure_values(&diff, DegenerateMeasure::Length)?;
let length = hypot(&diff);
ensure_finite_measure_value(length, DegenerateMeasure::Length)?;
if length == 0.0 {
return Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateFacet {
measure: DegenerateMeasure::Length,
degeneracy: DegenerateGeometry::CoincidentPoints,
},
});
}
Ok(length)
}
3 => {
let p0 = points[0].coords();
let p1 = points[1].coords();
let p2 = points[2].coords();
let v1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
let v2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
ensure_finite_measure_values(&v1, DegenerateMeasure::Area)?;
ensure_finite_measure_values(&v2, DegenerateMeasure::Area)?;
let cross = [
v1[2].mul_add(-v2[1], v1[1] * v2[2]),
v1[0].mul_add(-v2[2], v1[2] * v2[0]),
v1[1].mul_add(-v2[0], v1[0] * v2[1]),
];
ensure_finite_measure_values(&cross, DegenerateMeasure::Area)?;
let cross_magnitude = hypot(&cross);
let area = cross_magnitude / (2.0); ensure_finite_measure_value(cross_magnitude, DegenerateMeasure::Area)?;
ensure_finite_measure_value(area, DegenerateMeasure::Area)?;
let cross_scale = Float::abs(v1[1] * v2[2])
+ Float::abs(v1[2] * v2[1])
+ Float::abs(v1[2] * v2[0])
+ Float::abs(v1[0] * v2[2])
+ Float::abs(v1[0] * v2[1])
+ Float::abs(v1[1] * v2[0]);
ensure_finite_measure_value(cross_scale, DegenerateMeasure::Area)?;
if is_zero_or_roundoff(cross_magnitude, cross_scale) {
return Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateFacet {
measure: DegenerateMeasure::Area,
degeneracy: DegenerateGeometry::CollinearPoints,
},
});
}
Ok(area)
}
4 => {
facet_measure_gram_matrix::<D>(points)
}
_ => {
facet_measure_gram_matrix::<D>(points)
}
}
}
fn facet_measure_gram_matrix<const D: usize>(
points: &[Point<D>],
) -> Result<f64, CircumcenterError> {
let mut coords_f64 = [[0.0f64; D]; D];
for (dst, p) in coords_f64.iter_mut().zip(points.iter()) {
*dst = safe_coords_to_f64(p.coords())?;
}
let gram_dim = D - 1;
let det = try_with_la_stack_matrix!(gram_dim, |gram_matrix| {
for i in 0..gram_dim {
for j in 0..gram_dim {
let mut dot_product = 0.0;
for ((&ai, &aj), &a0) in coords_f64[i + 1]
.iter()
.zip(coords_f64[j + 1].iter())
.zip(coords_f64[0].iter())
{
let di = ai - a0;
let dj = aj - a0;
dot_product = di.mul_add(dj, dot_product);
}
matrix_set(&mut gram_matrix, i, j, dot_product)?;
}
}
validate_gram_determinant(gram_determinant_ldlt(gram_matrix)?)
})?;
let volume_f64 = {
let sqrt_det = det.sqrt();
let d_fact = factorial_f64(D - 1)?;
sqrt_det / d_fact
};
Ok(volume_f64)
}
pub fn surface_measure<U, V, const D: usize>(
facets: &[FacetView<'_, U, V, D>],
) -> Result<f64, SurfaceMeasureError> {
let mut total_measure = 0.0;
for facet in facets {
let points: Vec<Point<D>> = facet.vertices().map(|v| *v.point()).collect();
let measure = facet_measure(&points).map_err(SurfaceMeasureError::from)?;
total_measure += measure;
}
Ok(total_measure)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
core::{traits::facet_incidence_analysis::FacetIncidenceAnalysis, vertex::Vertex},
geometry::{matrix::LaError, point::Point, traits::coordinate::InvalidCoordinateValue},
triangulation::DelaunayTriangulation,
vertex,
};
use approx::assert_relative_eq;
use std::assert_matches;
#[test]
fn surface_measure_error_display_names_variants() {
let error = SurfaceMeasureError::from(CircumcenterError::EmptyPointSet);
let display = format!("{error}");
assert!(display.contains("Geometry computation failed"));
assert!(display.contains("Empty point set"));
}
#[test]
fn test_simplex_volume_1d_line_segment() {
let line = vec![
Point::try_new([0.0]).expect("finite point coordinates"),
Point::try_new([5.0]).expect("finite point coordinates"),
];
let volume = simplex_volume(&line).unwrap();
assert_relative_eq!(volume, 5.0, epsilon = 1e-10);
let line_neg = vec![
Point::try_new([5.0]).expect("finite point coordinates"),
Point::try_new([0.0]).expect("finite point coordinates"),
];
let volume_neg = simplex_volume(&line_neg).unwrap();
assert_relative_eq!(volume_neg, 5.0, epsilon = 1e-10);
}
#[test]
fn simplex_volume_degenerate_segment_errors() {
let line = vec![
Point::try_new([2.0]).expect("finite point coordinates"),
Point::try_new([2.0]).expect("finite point coordinates"),
];
assert_matches!(
simplex_volume(&line),
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateSimplex {
measure: DegenerateMeasure::Length,
degeneracy: DegenerateGeometry::CoincidentPoints,
}
})
);
}
#[test]
fn test_simplex_volume_2d_triangle() {
let triangle = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 4.0]).expect("finite point coordinates"),
];
let area = simplex_volume(&triangle).unwrap();
assert_relative_eq!(area, 6.0, epsilon = 1e-10);
let equilateral = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.5, 0.866_025]).expect("finite point coordinates"), ];
let area_eq = simplex_volume(&equilateral).unwrap();
assert_relative_eq!(area_eq, 0.433_013, epsilon = 1e-5);
}
#[test]
fn test_simplex_volume_3d_tetrahedron() {
let tetrahedron = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"),
];
let volume = simplex_volume(&tetrahedron).unwrap();
assert_relative_eq!(volume, 1.0 / 6.0, epsilon = 1e-10); }
#[test]
fn test_simplex_volume_4d_simplex() {
let simplex_4d = vec![
Point::try_new([0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 1.0]).expect("finite point coordinates"),
];
let volume = simplex_volume(&simplex_4d).unwrap();
assert_relative_eq!(volume, 1.0 / 24.0, epsilon = 1e-10);
}
#[test]
fn test_simplex_volume_degenerate() {
let collinear = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 1.0]).expect("finite point coordinates"),
Point::try_new([2.0, 2.0]).expect("finite point coordinates"),
];
let result = simplex_volume(&collinear);
assert_matches!(
result,
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateSimplex {
measure: DegenerateMeasure::Volume,
degeneracy: DegenerateGeometry::CollinearPoints,
},
}),
"Degenerate simplex should return a typed collinearity error"
);
}
#[test]
fn simplex_volume_non_finite_1d_intermediate_is_numeric_failure() {
let points = vec![
Point::try_new([-f64::MAX]).expect("finite point coordinates"),
Point::try_new([f64::MAX]).expect("finite point coordinates"),
];
assert_matches!(
simplex_volume(&points),
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NonFiniteMeasure {
measure: DegenerateMeasure::Length,
value: CoordinateConversionValue::NonFinite(
InvalidCoordinateValue::PositiveInfinity
),
},
})
);
}
#[test]
fn simplex_volume_non_finite_2d_intermediate_is_numeric_failure() {
let points = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([f64::MAX, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, f64::MAX]).expect("finite point coordinates"),
];
assert_matches!(
simplex_volume(&points),
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NonFiniteMeasure {
measure: DegenerateMeasure::Area,
value: CoordinateConversionValue::NonFinite(
InvalidCoordinateValue::PositiveInfinity
),
},
})
);
}
#[test]
fn simplex_volume_coplanar_tetrahedron_errors() {
let coplanar = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.25, 0.25, 0.0]).expect("finite point coordinates"),
];
let result = simplex_volume(&coplanar);
assert_matches!(
result,
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateSimplex {
measure: DegenerateMeasure::Volume,
degeneracy: DegenerateGeometry::CoplanarPoints,
},
}),
"coplanar tetrahedron should be a typed degeneracy"
);
}
#[test]
fn test_simplex_volume_very_small_valid_simplices() {
let small_val = 1e-14;
let triangle = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([small_val, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, small_val]).expect("finite point coordinates"),
];
let area = simplex_volume(&triangle).unwrap();
assert_relative_eq!(area, small_val * small_val / 2.0, max_relative = 1e-12);
let tetrahedron = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([small_val, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, small_val, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, small_val]).expect("finite point coordinates"),
];
let volume = simplex_volume(&tetrahedron).unwrap();
assert_relative_eq!(
volume,
small_val * small_val * small_val / 6.0,
max_relative = 1e-12
);
}
#[test]
fn test_simplex_volume_wrong_point_count() {
let points = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0]).expect("finite point coordinates"),
];
let result = simplex_volume::<2>(&points);
assert!(result.is_err());
}
#[test]
fn test_inradius_2d_equilateral_triangle() {
let triangle = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.5, 0.866_025]).expect("finite point coordinates"), ];
let r_in = inradius(&triangle).unwrap();
assert_relative_eq!(r_in, 0.288_675_13, epsilon = 1e-5);
}
#[test]
fn test_inradius_2d_right_triangle() {
let triangle = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 4.0]).expect("finite point coordinates"),
];
let r_in = inradius(&triangle).unwrap();
assert_relative_eq!(r_in, 1.0, epsilon = 1e-10);
}
#[test]
fn test_inradius_3d_regular_tetrahedron() {
let tetrahedron = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"),
];
let r_in = inradius(&tetrahedron).unwrap();
assert_relative_eq!(r_in, 0.2113, epsilon = 1e-3);
}
#[test]
fn test_inradius_degenerate() {
let collinear = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([2.0, 0.0]).expect("finite point coordinates"),
];
let result = inradius(&collinear);
assert!(result.is_err()); }
#[test]
fn test_facet_measure_1d_point() {
let points = vec![Point::try_new([5.0]).expect("finite point coordinates")];
let measure = facet_measure(&points).unwrap();
assert_relative_eq!(measure, 0.0, epsilon = 1e-10);
}
#[test]
fn test_facet_measure_2d_line_segment() {
let points = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 4.0]).expect("finite point coordinates"),
];
let measure = facet_measure(&points).unwrap();
assert_relative_eq!(measure, 5.0, epsilon = 1e-10);
}
#[test]
fn test_facet_measure_3d_triangle_right_angle() {
let points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 4.0, 0.0]).expect("finite point coordinates"),
];
let measure = facet_measure(&points).unwrap();
assert_relative_eq!(measure, 6.0, epsilon = 1e-10);
}
#[test]
fn test_facet_measure_4d_tetrahedron() {
let points = vec![
Point::try_new([0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0, 0.0]).expect("finite point coordinates"),
];
let measure = facet_measure(&points).unwrap();
assert_relative_eq!(measure, 1.0 / 6.0, epsilon = 1e-10);
}
fn gram_det_from_edges<const AMBIENT: usize>(
edges: &[[f64; AMBIENT]],
) -> Result<f64, CircumcenterError> {
let k = edges.len();
try_with_la_stack_matrix!(k, |gram_matrix| {
for i in 0..k {
for j in 0..k {
let mut dot_product = 0.0;
for (&a, &b) in edges[i].iter().zip(edges[j].iter()) {
dot_product = a.mul_add(b, dot_product);
}
matrix_set(&mut gram_matrix, i, j, dot_product)?;
}
}
validate_gram_determinant(gram_determinant_ldlt(gram_matrix)?)
})
}
#[test]
fn test_gram_determinant_ldlt_known_spd() {
let gram = Matrix::<2>::try_from_rows([[4.0, 2.0], [2.0, 3.0]]).unwrap();
let det = gram_determinant_ldlt(gram).unwrap();
assert_relative_eq!(det, 8.0, epsilon = 1e-12);
}
#[test]
fn gram_determinant_ldlt_preserves_determinant_overflow_error() {
let gram = Matrix::<5>::try_from_rows([
[1.0e100, 0.0, 0.0, 0.0, 0.0],
[0.0, 1.0e100, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0e100, 0.0, 0.0],
[0.0, 0.0, 0.0, 1.0e100, 0.0],
[0.0, 0.0, 0.0, 0.0, 1.0e100],
])
.unwrap();
assert_matches!(
gram_determinant_ldlt(gram),
Err(CircumcenterError::LinearAlgebraFailure {
source: LaError::NonFinite { .. }
})
);
}
#[test]
fn test_gram_determinant_parallel_edges_errors() {
let edges = [[1.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
assert!(gram_det_from_edges(&edges).is_err());
}
#[test]
fn degeneracy_tolerance_zero_scale() {
assert_relative_eq!(degeneracy_tolerance(0.0), 0.0);
assert_relative_eq!(degeneracy_tolerance(-1.0), 0.0);
}
#[test]
fn gram_determinant_nonfinite_errors() {
assert_matches!(
validate_gram_determinant(f64::NAN),
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NonFiniteGramDeterminant,
})
);
assert_matches!(
validate_gram_determinant(f64::INFINITY),
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NonFiniteGramDeterminant,
})
);
}
#[test]
fn gram_determinant_zero_reports_degenerate_volume() {
assert_matches!(
validate_gram_determinant(0.0),
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateSimplex {
measure: DegenerateMeasure::Volume,
degeneracy: DegenerateGeometry::CollinearOrCoplanarPoints,
},
})
);
}
#[test]
fn test_validate_gram_determinant_tiny_negative_errors() {
assert_matches!(
validate_gram_determinant(-1e-13),
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NegativeGramDeterminant,
})
);
}
macro_rules! test_gram_det_orthogonal {
($test_name:ident, $dim:literal) => {
#[test]
fn $test_name() {
let mut edges = [[0.0f64; $dim]; $dim];
for i in 0..$dim {
edges[i][i] = 1.0;
}
let det = gram_det_from_edges::<$dim>(&edges).unwrap();
assert_relative_eq!(det, 1.0, epsilon = 1e-10);
}
};
}
test_gram_det_orthogonal!(test_gram_determinant_orthogonal_2d, 2);
test_gram_det_orthogonal!(test_gram_determinant_orthogonal_3d, 3);
test_gram_det_orthogonal!(test_gram_determinant_orthogonal_4d, 4);
test_gram_det_orthogonal!(test_gram_determinant_orthogonal_5d, 5);
macro_rules! test_gram_det_scaled {
($test_name:ident, $dim:literal, $scale:expr, $expected_det:expr) => {
#[test]
fn $test_name() {
let mut edges = [[0.0f64; $dim]; $dim];
for i in 0..$dim {
edges[i][i] = $scale;
}
let det = gram_det_from_edges::<$dim>(&edges).unwrap();
assert_relative_eq!(det, $expected_det, epsilon = 1e-9);
}
};
}
test_gram_det_scaled!(test_gram_determinant_scaled_2d, 2, 2.0, 16.0); test_gram_det_scaled!(test_gram_determinant_scaled_3d, 3, 2.0, 64.0); test_gram_det_scaled!(test_gram_determinant_scaled_4d, 4, 2.0, 256.0); test_gram_det_scaled!(test_gram_determinant_scaled_5d, 5, 2.0, 1024.0);
#[test]
fn test_gram_matrix_debug() {
let triangle_3d = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0]).expect("finite point coordinates"),
];
let area_3d = facet_measure(&triangle_3d).unwrap();
assert_relative_eq!(area_3d, 0.5, epsilon = 1e-10);
#[cfg(feature = "diagnostics")]
tracing::debug!("3D triangle area: {area_3d} (expected: 0.5)");
let eps = 1e-10;
let near_singular = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, eps, 0.0]).expect("finite point coordinates"),
];
let area_ns = facet_measure(&near_singular).unwrap();
assert!(area_ns >= 0.0);
let area_3d_gram = facet_measure_gram_matrix::<3>(&triangle_3d).unwrap();
assert_relative_eq!(area_3d_gram, 0.5, epsilon = 1e-10);
#[cfg(feature = "diagnostics")]
tracing::debug!("3D triangle area (Gram): {area_3d_gram} (expected: 0.5)");
let tetrahedron_4d = vec![
Point::try_new([0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0, 0.0]).expect("finite point coordinates"),
];
let volume_4d = facet_measure(&tetrahedron_4d).unwrap();
assert_relative_eq!(volume_4d, 1.0 / 6.0, epsilon = 1e-10);
#[cfg(feature = "diagnostics")]
tracing::debug!(
"4D tetrahedron volume: {} (expected: {})",
volume_4d,
1.0 / 6.0
);
let volume_4d_gram = facet_measure_gram_matrix::<4>(&tetrahedron_4d).unwrap();
assert_relative_eq!(volume_4d_gram, 1.0 / 6.0, epsilon = 1e-10);
#[cfg(feature = "diagnostics")]
tracing::debug!(
"4D tetrahedron volume (Gram): {} (expected: {})",
volume_4d_gram,
1.0 / 6.0
);
}
#[test]
fn test_facet_measure_5d_simplex() {
let points = vec![
Point::try_new([0.0, 0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 1.0, 0.0]).expect("finite point coordinates"),
];
let measure = facet_measure(&points).unwrap();
assert_relative_eq!(measure, 1.0 / 24.0, epsilon = 1e-10);
}
#[test]
fn test_facet_measure_6d_simplex() {
let points = vec![
Point::try_new([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0]).expect("finite point coordinates"),
];
let measure = facet_measure(&points).unwrap();
assert_relative_eq!(measure, 1.0 / 120.0, epsilon = 1e-10);
}
#[test]
fn test_facet_measure_wrong_point_count() {
let points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
];
let result = facet_measure::<3>(&points);
assert!(result.is_err());
match result.unwrap_err() {
CircumcenterError::InvalidSimplex {
actual,
expected,
dimension,
} => {
assert_eq!(actual, 2);
assert_eq!(expected, 3);
assert_eq!(dimension, 3);
}
other => panic!("Expected InvalidSimplex error, got: {other:?}"),
}
}
#[test]
fn facet_measure_coincident_2d_points_reports_degenerate_length() {
let points = vec![
Point::try_new([1.0, -2.0]).expect("finite point coordinates"),
Point::try_new([1.0, -2.0]).expect("finite point coordinates"),
];
assert_matches!(
facet_measure(&points),
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::DegenerateFacet {
measure: DegenerateMeasure::Length,
degeneracy: DegenerateGeometry::CoincidentPoints,
},
})
);
}
#[test]
fn facet_measure_non_finite_2d_intermediate_is_numeric_failure() {
let points = vec![
Point::try_new([-f64::MAX, -f64::MAX]).expect("finite point coordinates"),
Point::try_new([f64::MAX, f64::MAX]).expect("finite point coordinates"),
];
assert_matches!(
facet_measure(&points),
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NonFiniteMeasure {
measure: DegenerateMeasure::Length,
value: CoordinateConversionValue::NonFinite(
InvalidCoordinateValue::PositiveInfinity
),
},
})
);
}
#[test]
fn facet_measure_non_finite_3d_intermediate_is_numeric_failure() {
let points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([f64::MAX, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, f64::MAX, 0.0]).expect("finite point coordinates"),
];
assert_matches!(
facet_measure(&points),
Err(CircumcenterError::MatrixInversionFailed {
reason: CircumcenterFailureReason::NonFiniteMeasure {
measure: DegenerateMeasure::Area,
value: CoordinateConversionValue::NonFinite(
InvalidCoordinateValue::PositiveInfinity
),
},
})
);
}
#[test]
fn test_facet_measure_zero_area_triangle() {
let points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([2.0, 0.0, 0.0]).expect("finite point coordinates"), ];
let result = facet_measure(&points);
assert!(result.is_err(), "Collinear points should return an error");
}
#[test]
fn test_facet_measure_nearly_collinear_points_2d() {
let eps = 1e-10;
let points = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, eps]).expect("finite point coordinates"), ];
let measure = facet_measure(&points).unwrap();
let expected = eps.mul_add(eps, 1.0).sqrt(); assert_relative_eq!(measure, expected, epsilon = 1e-9);
}
#[test]
fn test_facet_measure_nearly_coplanar_points_3d() {
let eps = 1e-8;
let points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, eps, eps]).expect("finite point coordinates"), ];
let measure = facet_measure(&points).unwrap();
assert!(
measure > 0.0,
"Nearly coplanar triangle should have positive area"
);
assert!(
measure < 1e-6,
"Nearly coplanar triangle should have very small area, got: {measure}"
);
}
#[test]
fn test_facet_measure_degenerate_4d_tetrahedron() {
let points = vec![
Point::try_new([0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.5, 0.5, 0.0, 0.0]).expect("finite point coordinates"), ];
let result = facet_measure(&points);
assert!(
result.is_err(),
"Degenerate 4D tetrahedron should return an error"
);
}
#[test]
fn test_surface_measure_empty_facets() {
let facets: Vec<FacetView<'_, (), (), 3>> = vec![];
let result = surface_measure(&facets).unwrap();
assert_relative_eq!(result, 0.0, epsilon = 1e-10);
}
#[test]
#[expect(
clippy::float_cmp,
reason = "Comparisons are against exact literals (constructed geometry), acceptable in this test"
)]
fn test_surface_measure_single_facet() {
let vertices: Vec<Vertex<(), 3>> = vec![
vertex!([0.0, 0.0, 0.0]).unwrap(), vertex!([3.0, 0.0, 0.0]).unwrap(), vertex!([0.0, 4.0, 0.0]).unwrap(), vertex!([0.0, 0.0, 1.0]).unwrap(), ];
let dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let boundary_facets: Vec<_> = dt
.tds()
.one_sided_facets()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let tarfacet = boundary_facets
.iter()
.find(|facet| {
let facet_vertices: Vec<_> = facet.vertices().collect();
facet_vertices.len() == 3
&& facet_vertices.iter().any(|v| {
let coords = *v.point().coords();
coords == [0.0, 0.0, 0.0]
})
&& facet_vertices.iter().any(|v| {
let coords = *v.point().coords();
coords == [3.0, 0.0, 0.0]
})
&& facet_vertices.iter().any(|v| {
let coords = *v.point().coords();
coords == [0.0, 4.0, 0.0]
})
})
.expect("Should find the target facet");
let surface_area = surface_measure(std::slice::from_ref(tarfacet)).unwrap();
assert_relative_eq!(surface_area, 6.0, epsilon = 1e-10);
}
#[test]
fn test_surface_measure_consistency_with_facet_measure() {
let vertices: Vec<Vertex<(), 3>> = 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(), vertex!([1.0, 1.0, 1.0]).unwrap(), ];
let dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let boundary_facets: Vec<_> = dt
.tds()
.one_sided_facets()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let facet1 = boundary_facets[0].clone();
let facet2 = boundary_facets[1].clone();
let total_surface = surface_measure(&[facet1.clone(), facet2.clone()]).unwrap();
let points1: Vec<Point<3>> = facet1
.vertices()
.map(|v| {
let coords = *v.point().coords();
Point::try_new(coords).expect("finite point coordinates")
})
.collect();
let points2: Vec<Point<3>> = facet2
.vertices()
.map(|v| {
let coords = *v.point().coords();
Point::try_new(coords).expect("finite point coordinates")
})
.collect();
let measure1 = facet_measure(&points1).unwrap();
let measure2 = facet_measure(&points2).unwrap();
let sum_individual = measure1 + measure2;
assert_relative_eq!(total_surface, sum_individual, epsilon = 1e-10);
}
#[test]
fn test_facet_measure_scaled_simplex_2d() {
let scale = 3.0;
let original_points = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0]).expect("finite point coordinates"),
];
let scaled_points = vec![
Point::try_new([0.0 * scale, 0.0 * scale]).expect("finite point coordinates"),
Point::try_new([1.0 * scale, 0.0 * scale]).expect("finite point coordinates"),
];
let original_measure = facet_measure(&original_points).unwrap();
let scaled_measure = facet_measure(&scaled_points).unwrap();
assert_relative_eq!(scaled_measure, original_measure * scale, epsilon = 1e-10);
}
#[test]
fn test_facet_measure_scaled_simplex_3d() {
let scale = 2.5;
let original_points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([2.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 3.0, 0.0]).expect("finite point coordinates"),
];
let scaled_points = vec![
Point::try_new([0.0 * scale, 0.0 * scale, 0.0 * scale])
.expect("finite point coordinates"),
Point::try_new([2.0 * scale, 0.0 * scale, 0.0 * scale])
.expect("finite point coordinates"),
Point::try_new([0.0 * scale, 3.0 * scale, 0.0 * scale])
.expect("finite point coordinates"),
];
let original_measure = facet_measure(&original_points).unwrap();
let scaled_measure = facet_measure(&scaled_points).unwrap();
assert_relative_eq!(
scaled_measure,
original_measure * scale * scale,
epsilon = 1e-10
);
}
#[test]
fn test_facet_measure_scaled_simplex_4d() {
let scale = 2.0;
let original_points = vec![
Point::try_new([0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0, 0.0]).expect("finite point coordinates"),
];
let scaled_points = vec![
Point::try_new([0.0 * scale, 0.0 * scale, 0.0 * scale, 0.0 * scale])
.expect("finite point coordinates"),
Point::try_new([1.0 * scale, 0.0 * scale, 0.0 * scale, 0.0 * scale])
.expect("finite point coordinates"),
Point::try_new([0.0 * scale, 1.0 * scale, 0.0 * scale, 0.0 * scale])
.expect("finite point coordinates"),
Point::try_new([0.0 * scale, 0.0 * scale, 1.0 * scale, 0.0 * scale])
.expect("finite point coordinates"),
];
let original_measure = facet_measure(&original_points).unwrap();
let scaled_measure = facet_measure(&scaled_points).unwrap();
assert_relative_eq!(
scaled_measure,
original_measure * scale.powi(3),
epsilon = 1e-10
);
}
#[test]
fn test_facet_measure_very_large_coordinates() {
let large_val = 1e8;
let points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([large_val, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, large_val, 0.0]).expect("finite point coordinates"),
];
let result = facet_measure(&points);
assert!(result.is_ok(), "Large coordinates should work");
let measure = result.unwrap();
assert!(measure.is_finite(), "Measure should be finite");
let expected = large_val * large_val / 2.0;
assert_relative_eq!(measure, expected, epsilon = 1e-3);
}
#[test]
fn test_facet_measure_very_small_coordinates() {
let small_val = 1e-14;
let points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([small_val, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, small_val, 0.0]).expect("finite point coordinates"),
];
let result = facet_measure(&points);
assert!(result.is_ok(), "Small coordinates should work");
let measure = result.unwrap();
assert!(measure.is_finite(), "Measure should be finite");
let expected = small_val * small_val / 2.0;
assert_relative_eq!(measure, expected, epsilon = 1e-40);
}
#[test]
fn test_facet_measure_mixed_positive_negative_coordinates() {
let points = vec![
Point::try_new([-1.0, -1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([2.0, -1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([-1.0, 3.0, 0.0]).expect("finite point coordinates"),
];
let measure = facet_measure(&points).unwrap();
assert_relative_eq!(measure, 6.0, epsilon = 1e-10);
}
#[test]
fn test_facet_measure_translation_invariance() {
let translation = [10.0, 20.0, 30.0];
let original_points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 4.0, 0.0]).expect("finite point coordinates"),
];
let translated_points = vec![
Point::try_new([
0.0 + translation[0],
0.0 + translation[1],
0.0 + translation[2],
])
.expect("finite point coordinates"),
Point::try_new([
3.0 + translation[0],
0.0 + translation[1],
0.0 + translation[2],
])
.expect("finite point coordinates"),
Point::try_new([
0.0 + translation[0],
4.0 + translation[1],
0.0 + translation[2],
])
.expect("finite point coordinates"),
];
let original_measure = facet_measure(&original_points).unwrap();
let translated_measure = facet_measure(&translated_points).unwrap();
assert_relative_eq!(original_measure, translated_measure, epsilon = 1e-10);
assert_relative_eq!(original_measure, 6.0, epsilon = 1e-10); }
#[test]
fn test_facet_measure_vertex_permutation_invariance() {
let points_order1 = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 4.0, 0.0]).expect("finite point coordinates"),
];
let points_order2 = vec![
Point::try_new([3.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 4.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
];
let points_order3 = vec![
Point::try_new([0.0, 4.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 0.0, 0.0]).expect("finite point coordinates"),
];
let measure1 = facet_measure(&points_order1).unwrap();
let measure2 = facet_measure(&points_order2).unwrap();
let measure3 = facet_measure(&points_order3).unwrap();
assert_relative_eq!(measure1, measure2, epsilon = 1e-10);
assert_relative_eq!(measure1, measure3, epsilon = 1e-10);
assert_relative_eq!(measure1, 6.0, epsilon = 1e-10);
}
#[test]
fn test_facet_measure_various_triangle_orientations() {
let triangles = [
vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0]).expect("finite point coordinates"),
],
vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"),
],
vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"),
],
vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 1.0]).expect("finite point coordinates"),
],
];
let expected_areas = [0.5, 0.5, 0.5];
for (i, triangle) in triangles.iter().take(3).enumerate() {
let measure = facet_measure(triangle).unwrap();
assert_relative_eq!(measure, expected_areas[i], epsilon = 1e-10);
}
let measure4 = facet_measure(&triangles[3]).unwrap();
assert!(
measure4 > 0.0,
"Diagonal triangle should have positive area"
);
assert!(
measure4.is_finite(),
"Diagonal triangle area should be finite"
);
}
#[test]
#[expect(
clippy::float_cmp,
reason = "Comparisons are against exact literals (constructed geometry), acceptable in this test"
)]
fn test_surface_measure_multiple_facets_different_sizes() {
let vertices1: Vec<Vertex<(), 3>> = 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 vertices2: Vec<Vertex<(), 3>> = vec![
vertex!([0.0, 0.0, 0.0]).unwrap(), vertex!([6.0, 0.0, 0.0]).unwrap(), vertex!([0.0, 8.0, 0.0]).unwrap(), vertex!([0.0, 0.0, 1.0]).unwrap(), ];
let dt1: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices1).build().unwrap();
let dt2: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices2).build().unwrap();
let boundary_facets1: Vec<_> = dt1
.tds()
.one_sided_facets()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let small_facet = boundary_facets1
.iter()
.find(|facet| {
let facet_vertices: Vec<_> = facet.vertices().collect();
facet_vertices.len() == 3
&& facet_vertices.iter().any(|v| {
let coords = *v.point().coords();
coords == [0.0, 0.0, 0.0]
})
&& facet_vertices.iter().any(|v| {
let coords = *v.point().coords();
coords == [1.0, 0.0, 0.0]
})
&& facet_vertices.iter().any(|v| {
let coords = *v.point().coords();
coords == [0.0, 1.0, 0.0]
})
})
.expect("Should find small triangle facet")
.clone();
let boundary_facets2: Vec<_> = dt2
.tds()
.one_sided_facets()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let large_facet = boundary_facets2
.iter()
.find(|facet| {
let facet_vertices: Vec<_> = facet.vertices().collect();
facet_vertices.len() == 3
&& facet_vertices.iter().any(|v| {
let coords = *v.point().coords();
coords == [0.0, 0.0, 0.0]
})
&& facet_vertices.iter().any(|v| {
let coords = *v.point().coords();
coords == [6.0, 0.0, 0.0]
})
&& facet_vertices.iter().any(|v| {
let coords = *v.point().coords();
coords == [0.0, 8.0, 0.0]
})
})
.expect("Should find large triangle facet")
.clone();
let total_surface = surface_measure(&[small_facet, large_facet]).unwrap();
let expected_total = 0.5 + 24.0;
assert_relative_eq!(total_surface, expected_total, epsilon = 1e-10);
}
#[test]
fn test_surface_measure_2d_perimeter() {
let vertices: Vec<Vertex<(), 2>> = vec![
vertex!([0.0, 0.0]).unwrap(), vertex!([3.0, 0.0]).unwrap(), vertex!([0.0, 4.0]).unwrap(), ];
let dt: DelaunayTriangulation<_, (), (), 2> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let boundary_facets: Vec<_> = dt
.tds()
.one_sided_facets()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let total_perimeter = surface_measure(&boundary_facets).unwrap();
assert_relative_eq!(total_perimeter, 12.0, epsilon = 1e-10);
}
#[test]
fn test_surface_measure_4d_boundary() {
let vertices: Vec<Vertex<(), 4>> = 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(), ];
let dt: DelaunayTriangulation<_, (), (), 4> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let boundary_facets: Vec<_> = dt
.tds()
.one_sided_facets()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let total_surface = surface_measure(&boundary_facets).unwrap();
let expected_total = 1.0;
assert_relative_eq!(total_surface, expected_total, epsilon = 1e-10);
}
#[test]
fn test_surface_measure_with_invalid_facet() {
let vertices: Vec<Vertex<(), 3>> = 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 boundary_facets: Vec<_> = dt
.tds()
.one_sided_facets()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
let result = surface_measure(&boundary_facets[0..1]);
assert!(result.is_ok(), "Valid facets should work");
let area = result.unwrap();
assert!(area > 0.0, "Area should be positive");
assert!(area.is_finite(), "Area should be finite");
}
#[test]
fn test_facet_measure_performance_many_dimensions() {
let points_7d = vec![
Point::try_new([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0]).expect("finite point coordinates"),
];
let measure_7d = facet_measure(&points_7d).unwrap();
assert_relative_eq!(measure_7d, 1.0 / 720.0, epsilon = 1e-10);
let points_8d = vec![
Point::try_new([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
.expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
.expect("finite point coordinates"),
Point::try_new([0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
.expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0])
.expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0])
.expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0])
.expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0])
.expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0])
.expect("finite point coordinates"),
];
let measure_8d = facet_measure(&points_8d).unwrap();
assert_relative_eq!(measure_8d, 1.0 / 5040.0, epsilon = 1e-10);
}
#[test]
fn test_surface_measure_many_facets() {
let vertices: Vec<Vertex<(), 3>> = vec![
vertex!([0.0, 0.0, 0.0]).unwrap(),
vertex!([2.0, 0.0, 0.0]).unwrap(),
vertex!([0.0, 2.0, 0.0]).unwrap(),
vertex!([0.0, 0.0, 2.0]).unwrap(),
];
let dt: DelaunayTriangulation<_, (), (), 3> =
DelaunayTriangulation::builder(&vertices).build().unwrap();
let boundary_facets: Vec<_> = dt
.tds()
.one_sided_facets()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(
boundary_facets.len(),
4,
"Tetrahedron should have 4 boundary facets, got {}",
boundary_facets.len()
);
let total_surface = surface_measure(&boundary_facets).unwrap();
assert!(total_surface.is_finite(), "Total surface should be finite");
assert!(total_surface > 0.0, "Total surface should be positive");
}
#[test]
fn test_facet_measure_equilateral_triangles() {
let side_lengths = [1.0, 2.0, 5.0, 10.0];
for &side in &side_lengths {
let height = side * 3.0_f64.sqrt() / 2.0;
let points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([side, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([side / 2.0, height, 0.0]).expect("finite point coordinates"),
];
let measure = facet_measure(&points).unwrap();
let expected_area = side * side * 3.0_f64.sqrt() / 4.0;
assert_relative_eq!(measure, expected_area, epsilon = 1e-10);
}
}
#[test]
fn test_facet_measure_regular_tetrahedron_faces() {
let side = 2.0;
let height = side * (2.0_f64 / 3.0).sqrt();
let center_offset = side / (2.0 * 3.0_f64.sqrt());
let v1 = Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates");
let v2 = Point::try_new([side, 0.0, 0.0]).expect("finite point coordinates");
let v3 = Point::try_new([side / 2.0, side * 3.0_f64.sqrt() / 2.0, 0.0])
.expect("finite point coordinates");
let v4 =
Point::try_new([side / 2.0, center_offset, height]).expect("finite point coordinates");
let faces = [
vec![v1, v2, v3], vec![v1, v2, v4], vec![v2, v3, v4], vec![v3, v1, v4], ];
let expected_face_area = side * side * 3.0_f64.sqrt() / 4.0;
for face in &faces {
let measure = facet_measure(face).unwrap();
assert_relative_eq!(measure, expected_face_area, epsilon = 1e-9);
}
}
#[test]
fn test_facet_measure_reflection_invariance() {
let original_points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 4.0, 0.0]).expect("finite point coordinates"),
];
let reflections = [
vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([-3.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 4.0, 0.0]).expect("finite point coordinates"),
],
vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, -4.0, 0.0]).expect("finite point coordinates"),
],
vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 4.0, 0.0]).expect("finite point coordinates"),
],
];
let original_measure = facet_measure(&original_points).unwrap();
for reflected_points in &reflections {
let reflected_measure = facet_measure(reflected_points).unwrap();
assert_relative_eq!(original_measure, reflected_measure, epsilon = 1e-10);
}
}
#[test]
fn test_facet_measure_rotation_invariance_2d() {
let original_points = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([3.0, 4.0]).expect("finite point coordinates"),
];
let rotated_points = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([-4.0, 3.0]).expect("finite point coordinates"), ];
let original_measure = facet_measure(&original_points).unwrap();
let rotated_measure = facet_measure(&rotated_points).unwrap();
assert_relative_eq!(original_measure, rotated_measure, epsilon = 1e-10);
assert_relative_eq!(original_measure, 5.0, epsilon = 1e-10); }
#[test]
fn test_facet_measure_gram_matrix_degenerate() {
let degenerate_points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([2.0, 0.0, 0.0]).expect("finite point coordinates"), ];
let result = facet_measure(°enerate_points);
if let Ok(measure) = result {
assert_relative_eq!(measure, 0.0, epsilon = 1e-10);
}
}
}