#![forbid(unsafe_code)]
use core::{array::from_fn, fmt, hint::cold_path};
use super::conversions::ValueConversionError;
use crate::geometry::matrix::{
DEFAULT_SINGULAR_TOL, ExactF64Conversion, LaError, LaVector, Matrix, MatrixError,
RationalMatrix, RationalVector, SingularityReason, StackMatrixDispatchError, matrix_set,
rational_from_f64,
};
use crate::geometry::point::Point;
use crate::geometry::traits::coordinate::{
CoordinateConversionError, CoordinateConversionValue, CoordinateValidationError,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DegenerateMeasure {
Length,
Area,
Volume,
SurfaceArea,
}
impl fmt::Display for DegenerateMeasure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Length => f.write_str("length"),
Self::Area => f.write_str("area"),
Self::Volume => f.write_str("volume"),
Self::SurfaceArea => f.write_str("surface area"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DegenerateGeometry {
CoincidentPoints,
CollinearPoints,
CoplanarPoints,
CollinearOrCoplanarPoints,
}
impl fmt::Display for DegenerateGeometry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CoincidentPoints => f.write_str("coincident points"),
Self::CollinearPoints => f.write_str("collinear points"),
Self::CoplanarPoints => f.write_str("coplanar points"),
Self::CollinearOrCoplanarPoints => f.write_str("collinear or coplanar points"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum CircumcenterFailureReason {
DegenerateSimplex {
measure: DegenerateMeasure,
degeneracy: DegenerateGeometry,
},
DegenerateFacet {
measure: DegenerateMeasure,
degeneracy: DegenerateGeometry,
},
NonFiniteGramDeterminant,
NegativeGramDeterminant,
NonPositiveSimplexMeasure {
measure: DegenerateMeasure,
value: CoordinateConversionValue,
},
NonFiniteMeasure {
measure: DegenerateMeasure,
value: CoordinateConversionValue,
},
}
impl fmt::Display for CircumcenterFailureReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DegenerateSimplex {
measure,
degeneracy,
} => write!(f, "degenerate simplex with zero {measure} ({degeneracy})"),
Self::DegenerateFacet {
measure,
degeneracy,
} => write!(f, "degenerate facet with zero {measure} ({degeneracy})"),
Self::NonFiniteGramDeterminant => f.write_str("Gram determinant is non-finite"),
Self::NegativeGramDeterminant => {
f.write_str("Gram matrix has negative determinant (degenerate simplex)")
}
Self::NonPositiveSimplexMeasure { measure, value } => {
write!(f, "degenerate simplex with {measure} ≈ {value}")
}
Self::NonFiniteMeasure { measure, value } => {
write!(f, "{measure} calculation produced non-finite value {value}")
}
}
}
}
#[derive(Clone, Copy, Debug, thiserror::Error, Eq, PartialEq)]
#[non_exhaustive]
pub enum ArrayConversionFailureReason {
#[error("array length mismatch")]
LengthMismatch,
}
#[derive(Clone, Debug, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum CircumcenterError {
#[error("Empty point set")]
EmptyPointSet,
#[error(
"Points do not form a valid simplex: expected {expected} points for dimension {dimension}, got {actual}"
)]
InvalidSimplex {
actual: usize,
expected: usize,
dimension: usize,
},
#[error("Ambient dimension {dimension} is below the required minimum {minimum}")]
DimensionTooSmall {
dimension: usize,
minimum: usize,
},
#[error("Invalid geometric measure: {reason}")]
InvalidMeasure {
reason: CircumcenterFailureReason,
},
#[error("Unsupported stack matrix dimension {requested} (maximum supported is {max})")]
UnsupportedMatrixDimension {
requested: usize,
max: usize,
},
#[error(
"Active matrix block size {active} does not match concrete matrix dimension {matrix_dimension}"
)]
MatrixDimensionMismatch {
active: usize,
matrix_dimension: usize,
},
#[error("Linear algebra failure: {source}")]
LinearAlgebraFailure {
#[source]
source: LaError,
},
#[error("Matrix error: {source}")]
MatrixError {
#[from]
source: MatrixError,
},
#[error("Array conversion failed: {reason}")]
ArrayConversionFailed {
reason: ArrayConversionFailureReason,
},
#[error("Coordinate conversion error: {source}")]
CoordinateConversion {
#[from]
source: CoordinateConversionError,
},
#[error("Coordinate validation error: {source}")]
CoordinateValidation {
#[from]
source: CoordinateValidationError,
},
#[error("Value conversion error: {source}")]
ValueConversion {
#[source]
source: Box<ValueConversionError>,
},
}
impl From<ValueConversionError> for CircumcenterError {
fn from(source: ValueConversionError) -> Self {
Self::ValueConversion {
source: Box::new(source),
}
}
}
impl From<StackMatrixDispatchError> for CircumcenterError {
fn from(source: StackMatrixDispatchError) -> Self {
match source {
StackMatrixDispatchError::UnsupportedDim { k, max } => {
Self::UnsupportedMatrixDimension { requested: k, max }
}
StackMatrixDispatchError::ActiveBlockDimensionMismatch { k, dim } => {
Self::MatrixDimensionMismatch {
active: k,
matrix_dimension: dim,
}
}
StackMatrixDispatchError::La { source } => Self::LinearAlgebraFailure { source },
StackMatrixDispatchError::Matrix { source } => Self::MatrixError { source },
}
}
}
impl From<LaError> for CircumcenterError {
fn from(source: LaError) -> Self {
Self::from(StackMatrixDispatchError::from(source))
}
}
pub fn circumcenter<const D: usize>(points: &[Point<D>]) -> Result<Point<D>, CircumcenterError> {
#[cfg(debug_assertions)]
if std::env::var_os("DELAUNAY_DEBUG_UNUSED_IMPORTS").is_some() {
tracing::debug!(
"circumsphere::circumcenter called (points_len={}, D={})",
points.len(),
D
);
}
if points.is_empty() {
return Err(CircumcenterError::EmptyPointSet);
}
let dim = points.len() - 1;
if dim != D {
return Err(CircumcenterError::InvalidSimplex {
actual: points.len(),
expected: D + 1,
dimension: D,
});
}
let coords_0 = points[0].coords();
let mut a = Matrix::<D>::zero();
let mut b_arr = [0.0f64; D];
let mut fast_system_is_finite = true;
for i in 0..D {
let coords_point = points[i + 1].coords();
let difference =
match LaVector::<D>::try_new(from_fn(|axis| coords_point[axis] - coords_0[axis])) {
Ok(difference) => difference,
Err(LaError::NonFinite { .. }) => {
fast_system_is_finite = false;
continue;
}
Err(source) => return Err(source.into()),
};
for (j, &coordinate) in difference.as_array().iter().enumerate() {
matrix_set(&mut a, i, j, coordinate)?;
}
match difference.norm_squared() {
Ok(squared_distance) => b_arr[i] = squared_distance,
Err(LaError::NonFinite { .. }) => fast_system_is_finite = false,
Err(source) => return Err(source.into()),
}
}
let circumcenter_coords = if fast_system_is_finite {
let b_vec = LaVector::<D>::try_new(b_arr)?;
match a.lu(DEFAULT_SINGULAR_TOL) {
Ok(lu) => {
let solution = lu
.solve(b_vec)
.map_err(CircumcenterError::from)?
.into_array();
from_fn(|index| 0.5_f64.mul_add(solution[index], coords_0[index]))
}
Err(LaError::Singular {
reason: SingularityReason::Numerical { .. },
..
}) => {
cold_path();
#[cfg(debug_assertions)]
if std::env::var_os("DELAUNAY_DEBUG_LU_FALLBACK").is_some() {
tracing::debug!(
"circumcenter<{D}>: LU near-singular, forming and solving the system rationally"
);
}
exact_circumcenter_from_points(points)?
}
Err(e) => {
cold_path();
return Err(e.into());
}
}
} else {
cold_path();
exact_circumcenter_from_points(points)?
};
Ok(Point::try_new(circumcenter_coords)?)
}
fn exact_circumcenter_from_points<const D: usize>(
points: &[Point<D>],
) -> Result<[f64; D], CircumcenterError> {
let reference = points[0].coords().map(|coordinate| {
rational_from_f64(coordinate).expect("validated Point coordinates are finite")
});
let zero = rational_from_f64(0.0).expect("zero is finite");
let half = rational_from_f64(0.5).expect("one half is finite");
let mut matrix = RationalMatrix::<D>::zero();
let mut rhs = from_fn(|_| zero.clone());
for row in 0..D {
for (column, origin) in reference.iter().enumerate() {
let coordinate = rational_from_f64(points[row + 1].coords()[column])
.expect("validated Point coordinates are finite");
let relative = coordinate - origin;
rhs[row] += relative.clone() * relative.clone();
matrix.set(row, column, relative)?;
}
}
let solution = matrix.solve(&RationalVector::try_new(rhs)?)?.into_array();
let center = RationalVector::<D>::try_from_fn(|index| {
reference[index].clone() + half.clone() * solution[index].clone()
})?;
Ok(center.to_rounded_f64()?.into_array())
}
pub fn circumradius<const D: usize>(points: &[Point<D>]) -> Result<f64, CircumcenterError> {
let circumcenter = circumcenter(points)?;
circumradius_with_center(points, &circumcenter)
}
pub fn circumradius_with_center<const D: usize>(
points: &[Point<D>],
circumcenter: &Point<D>,
) -> Result<f64, CircumcenterError> {
if points.is_empty() {
return Err(CircumcenterError::EmptyPointSet);
}
let point_coords = points[0].coords();
let circumcenter_coords = circumcenter.coords();
let mut diff_coords = [0.0; D];
for i in 0..D {
diff_coords[i] = circumcenter_coords[i] - point_coords[i];
}
Ok(LaVector::try_new(diff_coords)?.norm()?)
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use approx::assert_relative_eq;
use la_stack::UnrepresentableReason;
use super::*;
use crate::geometry::matrix::BigRational;
use crate::geometry::util::conversions::{ValueConversionFailureReason, safe_usize_to_scalar};
fn assert_stable_radius<const D: usize>() {
let origin = Point::try_new([0.0; D]).unwrap();
for scale in [1.0e-200, 1.0, 1.0e200] {
let mut coords = [0.0; D];
coords[0] = -3.0 * scale;
coords[1] = 4.0 * scale;
let point = Point::try_new(coords).unwrap();
let radius = circumradius_with_center(&[point], &origin).unwrap();
assert_relative_eq!(radius / scale, 5.0, max_relative = 1e-14);
}
for (large, small) in [
(1.0e200, 1.0e-200),
(f64::MIN_POSITIVE / 2.0, 0.0),
(f64::MAX, 0.0),
] {
let mut coords = [small; D];
coords[0] = large;
let point = Point::try_new(coords).unwrap();
let radius = circumradius_with_center(&[point], &origin).unwrap();
assert_relative_eq!(radius / large, 1.0, max_relative = 1e-14);
}
assert_relative_eq!(circumradius_with_center(&[origin], &origin).unwrap(), 0.0);
for scale in [1.0e-200, 1.0, 1.0e200] {
let mut simplex = vec![origin];
for axis in 0..D {
let mut coords = [0.0; D];
coords[axis] = scale;
simplex.push(Point::try_new(coords).unwrap());
}
let center = circumcenter(&simplex).unwrap();
for coordinate in center.coords() {
assert_relative_eq!(coordinate / scale, 0.5, max_relative = 1e-14);
}
let expected = safe_usize_to_scalar(D).unwrap().sqrt() / 2.0;
assert_relative_eq!(
circumradius(&simplex).unwrap() / scale,
expected,
max_relative = 1e-14
);
}
}
fn assert_rational_circumcenter_round_trip<const D: usize>() {
for (origin, opposite, expected) in [(1.25, -0.75, 0.25), (-1.0e308, 1.0e308, 0.0)] {
let mut points = vec![Point::try_new([origin; D]).unwrap()];
for axis in 0..D {
let mut coords = [origin; D];
coords[axis] = opposite;
points.push(Point::try_new(coords).unwrap());
}
points.swap(1, D);
let center = exact_circumcenter_from_points(&points).unwrap();
for coordinate in center {
assert_relative_eq!(coordinate, expected);
}
let squared_distances: Vec<_> = points
.iter()
.map(|point| {
point
.coords()
.iter()
.zip(center)
.map(|(&coordinate, center)| {
let delta = rational_from_f64(coordinate).unwrap()
- rational_from_f64(center).unwrap();
&delta * &delta
})
.sum::<BigRational>()
})
.collect();
assert!(
squared_distances
.iter()
.all(|distance| *distance == squared_distances[0])
);
if origin.abs() > 1.0e300 {
let public_center = circumcenter(&points).unwrap();
for coordinate in public_center.coords() {
assert_relative_eq!(*coordinate, expected);
}
}
}
}
macro_rules! rational_circumcenter_test {
($dimension:literal) => {
pastey::paste! {
#[test]
fn [<rational_circumcenter_round_trip_ $dimension d>]() {
assert_rational_circumcenter_round_trip::<$dimension>();
}
#[test]
fn [<stable_radius_ $dimension d>]() {
assert_stable_radius::<$dimension>();
}
}
};
}
rational_circumcenter_test!(2);
rational_circumcenter_test!(3);
rational_circumcenter_test!(4);
rational_circumcenter_test!(5);
rational_circumcenter_test!(6);
#[test]
fn rational_circumcenter_reports_unrepresentable_coordinate() {
let points = [
Point::try_new([0.0, 0.0]).unwrap(),
Point::try_new([1.0, 0.0]).unwrap(),
Point::try_new([0.5, f64::from_bits(1)]).unwrap(),
];
assert_matches!(
circumcenter(&points),
Err(CircumcenterError::LinearAlgebraFailure {
source: LaError::Unrepresentable {
index: Some(1),
reason: UnrepresentableReason::NotFinite,
..
},
})
);
}
#[test]
fn circumcenter_error_display_names_variants() {
let empty_error = CircumcenterError::EmptyPointSet;
let display = format!("{empty_error}");
assert!(display.contains("Empty point set"));
let simplex_error = CircumcenterError::InvalidSimplex {
actual: 2,
expected: 3,
dimension: 2,
};
let display = format!("{simplex_error}");
assert!(display.contains("Points do not form a valid simplex"));
}
#[test]
fn degenerate_measure_display_names_all_variants() {
assert_eq!(DegenerateMeasure::Length.to_string(), "length");
assert_eq!(DegenerateMeasure::Area.to_string(), "area");
assert_eq!(DegenerateMeasure::Volume.to_string(), "volume");
assert_eq!(DegenerateMeasure::SurfaceArea.to_string(), "surface area");
}
#[test]
fn degenerate_geometry_display_names_all_variants() {
assert_eq!(
DegenerateGeometry::CoincidentPoints.to_string(),
"coincident points"
);
assert_eq!(
DegenerateGeometry::CollinearPoints.to_string(),
"collinear points"
);
assert_eq!(
DegenerateGeometry::CoplanarPoints.to_string(),
"coplanar points"
);
assert_eq!(
DegenerateGeometry::CollinearOrCoplanarPoints.to_string(),
"collinear or coplanar points"
);
}
#[test]
fn circumcenter_failure_reason_display_preserves_typed_payloads() {
let degenerate_simplex = CircumcenterFailureReason::DegenerateSimplex {
measure: DegenerateMeasure::Volume,
degeneracy: DegenerateGeometry::CoplanarPoints,
};
assert_eq!(
degenerate_simplex.to_string(),
"degenerate simplex with zero volume (coplanar points)"
);
let degenerate_facet = CircumcenterFailureReason::DegenerateFacet {
measure: DegenerateMeasure::Length,
degeneracy: DegenerateGeometry::CoincidentPoints,
};
assert_eq!(
degenerate_facet.to_string(),
"degenerate facet with zero length (coincident points)"
);
assert_eq!(
CircumcenterFailureReason::NonFiniteGramDeterminant.to_string(),
"Gram determinant is non-finite"
);
assert_eq!(
CircumcenterFailureReason::NegativeGramDeterminant.to_string(),
"Gram matrix has negative determinant (degenerate simplex)"
);
assert_eq!(
CircumcenterFailureReason::NonPositiveSimplexMeasure {
measure: DegenerateMeasure::SurfaceArea,
value: CoordinateConversionValue::from_f64(0.0),
}
.to_string(),
"degenerate simplex with surface area ≈ 0.0"
);
assert_eq!(
CircumcenterFailureReason::NonFiniteMeasure {
measure: DegenerateMeasure::Volume,
value: CoordinateConversionValue::from_f64(f64::INFINITY),
}
.to_string(),
"volume calculation produced non-finite value inf"
);
}
#[test]
fn circumcenter_error_conversions_preserve_typed_payloads() {
let value_error = ValueConversionError::ConversionFailed {
value: CoordinateConversionValue::from_usize(4),
from_type: "usize",
to_type: "f64",
reason: ValueConversionFailureReason::TargetTypeRejected,
};
assert_matches!(
CircumcenterError::from(value_error),
CircumcenterError::ValueConversion { source }
if matches!(
*source,
ValueConversionError::ConversionFailed {
value: CoordinateConversionValue::UnsignedInteger(4),
from_type: "usize",
to_type: "f64",
reason: ValueConversionFailureReason::TargetTypeRejected,
}
)
);
assert_eq!(
CircumcenterError::from(StackMatrixDispatchError::ActiveBlockDimensionMismatch {
k: 4,
dim: 3,
}),
CircumcenterError::MatrixDimensionMismatch {
active: 4,
matrix_dimension: 3,
}
);
}
#[test]
fn predicates_circumcenter() {
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([0.0, 1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"),
];
let center = circumcenter(&points).unwrap();
assert_eq!(
center,
Point::try_new([0.5, 0.5, 0.5]).expect("finite point coordinates")
);
}
#[test]
fn predicates_circumcenter_fail() {
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([0.0, 1.0, 0.0]).expect("finite point coordinates"),
];
let center = circumcenter(&points);
assert!(center.is_err());
}
#[test]
fn predicates_circumradius() {
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([0.0, 1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"),
];
let radius = circumradius(&points).unwrap();
let expected_radius: f64 = 3.0_f64.sqrt() / 2.0;
assert_relative_eq!(radius, expected_radius, epsilon = 1e-9);
}
#[test]
fn circumradius_rejects_unrepresentable_finite_tetrahedron_radius() {
let magnitude = 1.1e308;
let points = vec![
Point::try_new([-magnitude, -magnitude, -magnitude]).expect("finite point coordinates"),
Point::try_new([magnitude, -magnitude, -magnitude]).expect("finite point coordinates"),
Point::try_new([-magnitude, magnitude, -magnitude]).expect("finite point coordinates"),
Point::try_new([-magnitude, -magnitude, magnitude]).expect("finite point coordinates"),
];
assert_matches!(
circumradius(&points),
Err(CircumcenterError::LinearAlgebraFailure {
source: LaError::NonFinite { .. },
})
);
}
#[test]
fn predicates_circumcenter_2d() {
let points = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([2.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0, 2.0]).expect("finite point coordinates"),
];
let center = circumcenter(&points).unwrap();
assert_relative_eq!(center.coords()[0], 1.0, epsilon = 1e-10);
assert_relative_eq!(center.coords()[1], 0.75, epsilon = 1e-10);
}
#[test]
fn test_circumradius_with_center_empty_point_set() {
let points: Vec<Point<3>> = Vec::new();
let center = Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates");
match circumradius_with_center(&points, ¢er) {
Err(CircumcenterError::EmptyPointSet) => {}
other => panic!("expected EmptyPointSet, got {other:?}"),
}
}
#[test]
fn predicates_circumradius_2d() {
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"),
Point::try_new([0.0, 1.0]).expect("finite point coordinates"),
];
let radius = circumradius(&points).unwrap();
let expected_radius = 2.0_f64.sqrt() / 2.0;
assert_relative_eq!(radius, expected_radius, epsilon = 1e-10);
}
#[test]
fn predicates_circumradius_with_center() {
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([0.0, 1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"),
];
let center = circumcenter(&points).unwrap();
let radius_with_center = circumradius_with_center(&points, ¢er);
let radius_direct = circumradius(&points).unwrap();
assert_relative_eq!(radius_with_center.unwrap(), radius_direct, epsilon = 1e-10);
}
#[test]
fn test_circumcenter_regular_simplex_3d() {
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([0.5, 3.0_f64.sqrt() / 2.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.5, 3.0_f64.sqrt() / 6.0, (2.0 / 3.0_f64).sqrt()])
.expect("finite point coordinates"),
];
let center = circumcenter(&points).unwrap();
let center_coords = center.coords();
for coord in center_coords {
assert!(
coord.is_finite(),
"Circumcenter coordinates should be finite"
);
}
let mut distances = points.iter().map(|p| {
let p_coords = *p.coords();
let diff = [
p_coords[0] - center_coords[0],
p_coords[1] - center_coords[1],
p_coords[2] - center_coords[2],
];
LaVector::try_new(diff).unwrap().norm().unwrap()
});
let first_distance = distances.next().expect("fixture has vertices");
for distance in distances {
assert_relative_eq!(first_distance, distance, epsilon = 1e-10);
}
}
#[test]
fn test_circumcenter_regular_simplex_4d() {
let points: Vec<Point<4>> = 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 center = circumcenter(&points).unwrap();
let center_coords = center.coords();
for &coord in center_coords {
assert!(
coord.is_finite(),
"Circumcenter coordinates should be finite"
);
assert_relative_eq!(coord, 0.5, epsilon = 1e-9);
}
}
#[test]
fn test_circumcenter_right_triangle_2d() {
let points = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([4.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 3.0]).expect("finite point coordinates"),
];
let center = circumcenter(&points).unwrap();
let center_coords = center.coords();
assert_relative_eq!(center_coords[0], 2.0, epsilon = 1e-10);
assert_relative_eq!(center_coords[1], 1.5, epsilon = 1e-10);
}
#[test]
fn test_circumcenter_scaled_simplex() {
let scale = 10.0;
let points = vec![
Point::try_new([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])
.expect("finite point coordinates"),
Point::try_new([0.0 * scale, 1.0 * scale, 0.0 * scale])
.expect("finite point coordinates"),
Point::try_new([0.0 * scale, 0.0 * scale, 1.0 * scale])
.expect("finite point coordinates"),
];
let center = circumcenter(&points).unwrap();
let expected_center = Point::try_new([0.5 * scale, 0.5 * scale, 0.5 * scale])
.expect("finite point coordinates");
let center_coords = center.coords();
let expected_coords = expected_center.coords();
for i in 0..3 {
assert_relative_eq!(center_coords[i], expected_coords[i], epsilon = 1e-9);
}
}
#[test]
fn test_circumcenter_translated_simplex() {
let translation = [10.0, 20.0, 30.0];
let points = vec![
Point::try_new([
0.0 + translation[0],
0.0 + translation[1],
0.0 + translation[2],
])
.expect("finite point coordinates"),
Point::try_new([
1.0 + translation[0],
0.0 + translation[1],
0.0 + translation[2],
])
.expect("finite point coordinates"),
Point::try_new([
0.0 + translation[0],
1.0 + translation[1],
0.0 + translation[2],
])
.expect("finite point coordinates"),
Point::try_new([
0.0 + translation[0],
0.0 + translation[1],
1.0 + translation[2],
])
.expect("finite point coordinates"),
];
let center = circumcenter(&points).unwrap();
let untranslated_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([0.0, 1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 1.0]).expect("finite point coordinates"),
];
let untranslated_center = circumcenter(&untranslated_points).unwrap();
let center_coords = center.coords();
let untranslated_coords = untranslated_center.coords();
for i in 0..3 {
assert_relative_eq!(
center_coords[i],
untranslated_coords[i] + translation[i],
epsilon = 1e-9
);
}
let expected = [10.5, 20.5, 30.5];
for i in 0..3 {
assert_relative_eq!(center_coords[i], expected[i], epsilon = 1e-9);
}
}
#[test]
fn test_circumcenter_nearly_degenerate_simplex() {
let eps = 1e-3;
let points: Vec<Point<3>> = 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.5, eps, 0.0]).expect("finite point coordinates"), Point::try_new([0.5, 0.0, eps]).expect("finite point coordinates"), ];
let center = circumcenter(&points)
.expect("finite non-degenerate tetrahedron must have a circumcenter");
let off_axis = eps.mul_add(eps, -0.25) / (2.0 * eps);
for (&actual, expected) in center.coords().iter().zip([0.5, off_axis, off_axis]) {
assert_relative_eq!(actual, expected, epsilon = 0.0, max_relative = 1e-14);
}
}
#[test]
fn test_circumcenter_wrong_dimension() {
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 = circumcenter(&points);
assert!(result.is_err());
match result.unwrap_err() {
CircumcenterError::InvalidSimplex {
actual,
expected,
dimension,
} => {
assert_eq!(actual, 2);
assert_eq!(expected, 4); assert_eq!(dimension, 3);
}
other => panic!("Expected InvalidSimplex error, got: {other:?}"),
}
}
#[test]
fn test_circumcenter_equilateral_triangle_properties() {
let side_length = 2.0;
let height = side_length * 3.0_f64.sqrt() / 2.0;
let points = vec![
Point::try_new([0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([side_length, 0.0]).expect("finite point coordinates"),
Point::try_new([side_length / 2.0, height]).expect("finite point coordinates"),
];
let center = circumcenter(&points).unwrap();
let center_coords = center.coords();
let expected_x = side_length / 2.0;
let expected_y = height / 3.0;
assert_relative_eq!(center_coords[0], expected_x, epsilon = 1e-10);
assert_relative_eq!(center_coords[1], expected_y, epsilon = 1e-10);
let mut distances = points.iter().map(|p| {
let p_coords = *p.coords();
let diff = [
p_coords[0] - center_coords[0],
p_coords[1] - center_coords[1],
];
LaVector::try_new(diff).unwrap().norm().unwrap()
});
let first_distance = distances.next().expect("fixture has vertices");
for distance in distances {
assert_relative_eq!(first_distance, distance, epsilon = 1e-10);
}
}
#[test]
fn test_circumcenter_numerical_stability() {
let points: Vec<Point<2>> = vec![
Point::try_new([1.0, 0.0]).expect("finite point coordinates"),
Point::try_new([1.000_000_1, 0.0]).expect("finite point coordinates"), Point::try_new([1.000_000_1, 0.000_000_1]).expect("finite point coordinates"), ];
let center = circumcenter(&points)
.expect("finite non-degenerate right triangle must have a circumcenter");
for (&actual, expected) in center.coords().iter().zip([1.000_000_05, 5e-8]) {
assert_relative_eq!(actual, expected, epsilon = 0.0, max_relative = 1e-14);
}
}
#[test]
fn test_circumcenter_1d_case() {
let points = vec![
Point::try_new([0.0]).expect("finite point coordinates"),
Point::try_new([2.0]).expect("finite point coordinates"),
];
let center = circumcenter(&points).unwrap();
let center_coords = center.coords();
assert_relative_eq!(center_coords[0], 1.0, epsilon = 1e-10);
}
#[test]
fn test_circumcenter_high_dimension() {
let points: Vec<Point<5>> = 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"),
Point::try_new([0.0, 0.0, 0.0, 0.0, 1.0]).expect("finite point coordinates"),
];
let result = circumcenter(&points);
assert!(result.is_ok(), "5D circumcenter should work");
let center = result.unwrap();
let center_coords = center.coords();
for coord in center_coords {
assert!(
coord.is_finite(),
"Circumcenter coordinates should be finite"
);
}
let mut distances = points.iter().map(|p| {
let p_coords = *p.coords();
let diff = [
p_coords[0] - center_coords[0],
p_coords[1] - center_coords[1],
p_coords[2] - center_coords[2],
p_coords[3] - center_coords[3],
p_coords[4] - center_coords[4],
];
LaVector::try_new(diff).unwrap().norm().unwrap()
});
let first_distance = distances.next().expect("fixture has vertices");
for distance in distances {
assert_relative_eq!(first_distance, distance, epsilon = 1e-9);
}
}
#[test]
fn predicates_circumcenter_precise_values() {
let points = vec![
Point::try_new([0.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([6.0, 0.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 8.0, 0.0]).expect("finite point coordinates"),
Point::try_new([0.0, 0.0, 10.0]).expect("finite point coordinates"),
];
let center = circumcenter(&points).unwrap();
let center_coords = center.coords();
assert_relative_eq!(center_coords[0], 3.0, epsilon = 1e-10);
assert_relative_eq!(center_coords[1], 4.0, epsilon = 1e-10);
assert_relative_eq!(center_coords[2], 5.0, epsilon = 1e-10);
}
#[test]
fn test_circumcenter_empty_point_set() {
let empty_points: Vec<Point<3>> = vec![];
let result = circumcenter(&empty_points);
assert_matches!(result, Err(CircumcenterError::EmptyPointSet));
}
#[test]
fn test_circumcenter_invalid_simplex() {
let points_2d = 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 = circumcenter(&points_2d);
assert_matches!(result, Err(CircumcenterError::InvalidSimplex { .. }));
let points_extra = 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.0, 1.0]).expect("finite point coordinates"),
Point::try_new([0.5, 0.5]).expect("finite point coordinates"), ];
let result = circumcenter(&points_extra);
assert_matches!(result, Err(CircumcenterError::InvalidSimplex { .. }));
}
#[test]
fn test_circumcenter_degenerate_matrix() {
let collinear_points = 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 = circumcenter(&collinear_points);
assert_matches!(
result,
Err(CircumcenterError::LinearAlgebraFailure {
source: LaError::Singular { .. }
})
);
}
#[test]
fn test_circumcenter_exact_fallback_near_singular_3d() {
let eps = 1e-14; let points: Vec<Point<3>> = 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.5, 0.5, eps]).expect("finite point coordinates"), ];
let system = Matrix::try_from_rows([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.5, 0.5, eps]])
.expect("finite system matrix");
assert_matches!(
system.lu(DEFAULT_SINGULAR_TOL),
Err(LaError::Singular {
reason: SingularityReason::Numerical { .. },
..
})
);
let center =
circumcenter(&points).expect("exact fallback should handle near-singular system");
let expected_z = eps.mul_add(eps, -0.5) / (2.0 * eps);
for (&actual, expected) in center.coords().iter().zip([0.5, 0.5, expected_z]) {
assert_relative_eq!(actual, expected, epsilon = 0.0, max_relative = 1e-14);
}
}
#[test]
fn test_circumcenter_exact_fallback_near_singular_2d() {
let eps = 1e-15;
let points: Vec<Point<2>> = 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, eps]).expect("finite point coordinates"), ];
let system = Matrix::try_from_rows([[1.0, 0.0], [0.5, eps]]).expect("finite system matrix");
assert_matches!(
system.lu(DEFAULT_SINGULAR_TOL),
Err(LaError::Singular {
reason: SingularityReason::Numerical { .. },
..
})
);
let center =
circumcenter(&points).expect("exact fallback should handle near-singular 2D system");
let expected_y = eps.mul_add(eps, -0.25) / (2.0 * eps);
for (&actual, expected) in center.coords().iter().zip([0.5, expected_y]) {
assert_relative_eq!(actual, expected, epsilon = 0.0, max_relative = 1e-14);
}
}
#[test]
fn test_circumcenter_rational_system_handles_finite_difference_overflow() {
let points = [
Point::try_new([-1.0e308, 0.0]).expect("finite point coordinates"),
Point::try_new([1.0e308, 0.0]).expect("finite point coordinates"),
Point::try_new([-1.0e308, 2.0]).expect("finite point coordinates"),
];
let center = circumcenter(&points)
.expect("finite source coordinates should bypass overflowing f64 differences");
assert_relative_eq!(center.coords()[0], 0.0);
assert_relative_eq!(center.coords()[1], 1.0);
}
}