#![forbid(unsafe_code)]
use super::predicates::{InSphere, Orientation};
use super::util::{safe_coords_to_f64, safe_scalar_to_f64, squared_norm};
use crate::geometry::matrix::matrix_set;
use crate::geometry::point::Point;
use crate::geometry::traits::coordinate::{
Coordinate, CoordinateConversionError, CoordinateScalar, ScalarSummable,
};
use num_traits::cast;
use std::fmt::Debug;
use std::sync::LazyLock;
static STRICT_INSPHERE_CONSISTENCY: LazyLock<bool> =
LazyLock::new(|| std::env::var_os("DELAUNAY_STRICT_INSPHERE_CONSISTENCY").is_some());
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConsistencyResult {
Consistent,
Inconsistent(InsphereConsistencyError),
Unverifiable,
}
impl ConsistencyResult {
#[must_use]
pub const fn is_consistent(self) -> bool {
matches!(self, Self::Consistent | Self::Unverifiable)
}
}
impl std::fmt::Display for ConsistencyResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Consistent => write!(f, "Consistent"),
Self::Inconsistent(error) => write!(f, "{error}"),
Self::Unverifiable => write!(f, "Unverifiable"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum InsphereConsistencyError {
#[error(
"Insphere inconsistency: determinant={determinant_result:?}, distance={distance_result:?}"
)]
DirectContradiction {
determinant_result: InSphere,
distance_result: InSphere,
},
}
#[derive(Debug, Clone)]
pub struct RobustPredicateConfig<T> {
pub base_tolerance: T,
pub relative_tolerance_factor: T,
pub max_refinement_iterations: usize,
pub exact_arithmetic_threshold: T,
pub perturbation_scale: T,
pub visibility_threshold_multiplier: T,
}
impl<T: CoordinateScalar> Default for RobustPredicateConfig<T> {
fn default() -> Self {
Self {
base_tolerance: T::default_tolerance(),
relative_tolerance_factor: cast(1e-12).unwrap_or_else(T::default_tolerance),
max_refinement_iterations: 3,
exact_arithmetic_threshold: cast(1e-10).unwrap_or_else(T::default_tolerance),
perturbation_scale: cast(1e-10).unwrap_or_else(T::default_tolerance),
visibility_threshold_multiplier: cast(100.0)
.unwrap_or_else(|| T::from(100.0).unwrap_or_else(T::default_tolerance)),
}
}
}
pub fn robust_insphere<T, const D: usize>(
simplex_points: &[Point<T, D>],
test_point: &Point<T, D>,
config: &RobustPredicateConfig<T>,
) -> Result<InSphere, CoordinateConversionError>
where
T: ScalarSummable,
[T; D]: Copy + Sized,
{
if simplex_points.len() != D + 1 {
return Err(CoordinateConversionError::ConversionFailed {
coordinate_index: 0,
coordinate_value: format!("Expected {} points, got {}", D + 1, simplex_points.len()),
from_type: "point count",
to_type: "valid simplex",
});
}
super::util::safe_scalar_to_f64(config.base_tolerance)?;
if let Ok(result) = adaptive_tolerance_insphere(simplex_points, test_point, config) {
if let ConsistencyResult::Inconsistent(error) =
verify_insphere_consistency(simplex_points, test_point, result, config)
{
if *STRICT_INSPHERE_CONSISTENCY {
let details = format!(
"{error}; simplex_points={simplex_points:?}; test_point={test_point:?}"
);
return Err(CoordinateConversionError::InsphereInconsistency {
simplex_points: format!("{simplex_points:?}"),
test_point: format!("{test_point:?}"),
details,
});
}
}
return Ok(result);
}
Ok(symbolic_perturbation_insphere(
simplex_points,
test_point,
config,
))
}
#[inline]
fn fill_insphere_predicate_matrix<T, const D: usize, const K: usize>(
matrix: &mut crate::geometry::matrix::Matrix<K>,
simplex_points: &[Point<T, D>],
test_point: &Point<T, D>,
) -> Result<(), CoordinateConversionError>
where
T: CoordinateScalar,
[T; D]: Copy + Sized,
{
debug_assert_eq!(K, D + 2);
for (i, point) in simplex_points.iter().enumerate() {
let coords = point.coords();
let coords_f64 = safe_coords_to_f64(coords)?;
for (j, &v) in coords_f64.iter().enumerate() {
matrix_set(matrix, i, j, v);
}
let norm_sq = squared_norm(coords);
let norm_sq_f64 = safe_scalar_to_f64(norm_sq)?;
matrix_set(matrix, i, D, norm_sq_f64);
matrix_set(matrix, i, D + 1, 1.0);
}
let test_coords = test_point.coords();
let test_coords_f64 = safe_coords_to_f64(test_coords)?;
for (j, &v) in test_coords_f64.iter().enumerate() {
matrix_set(matrix, D + 1, j, v);
}
let test_norm_sq = squared_norm(test_coords);
let test_norm_sq_f64 = safe_scalar_to_f64(test_norm_sq)?;
matrix_set(matrix, D + 1, D, test_norm_sq_f64);
matrix_set(matrix, D + 1, D + 1, 1.0);
Ok(())
}
fn adaptive_tolerance_insphere<T, const D: usize>(
simplex_points: &[Point<T, D>],
test_point: &Point<T, D>,
config: &RobustPredicateConfig<T>,
) -> Result<InSphere, CoordinateConversionError>
where
T: CoordinateScalar,
[T; D]: Copy + Sized,
{
let base_tol = super::util::safe_scalar_to_f64(config.base_tolerance)?;
let orientation = robust_orientation(simplex_points, config)?;
if matches!(orientation, Orientation::DEGENERATE) {
return Ok(InSphere::BOUNDARY);
}
let orient_sign: i8 = if matches!(orientation, Orientation::POSITIVE) {
1
} else {
-1
};
let k = D + 2;
try_with_la_stack_matrix!(k, |matrix| {
fill_insphere_predicate_matrix(&mut matrix, simplex_points, test_point)?;
Ok(super::predicates::insphere_from_matrix(
&matrix,
k,
orient_sign,
base_tol,
))
})
}
fn symbolic_perturbation_insphere<T, const D: usize>(
simplex_points: &[Point<T, D>],
test_point: &Point<T, D>,
config: &RobustPredicateConfig<T>,
) -> InSphere
where
T: ScalarSummable,
[T; D]: Copy + Sized,
{
let perturbation_directions = generate_perturbation_directions::<T, D>();
for direction in perturbation_directions {
let perturbed_test_point =
apply_perturbation(test_point, direction, config.perturbation_scale);
match adaptive_tolerance_insphere(simplex_points, &perturbed_test_point, config) {
Ok(InSphere::INSIDE) => return InSphere::INSIDE,
Ok(InSphere::OUTSIDE) => return InSphere::OUTSIDE,
Ok(InSphere::BOUNDARY) | Err(_) => {} }
}
geometric_deterministic_tie_breaking(simplex_points, test_point)
}
pub fn robust_orientation<T, const D: usize>(
simplex_points: &[Point<T, D>],
config: &RobustPredicateConfig<T>,
) -> Result<Orientation, CoordinateConversionError>
where
T: CoordinateScalar,
[T; D]: Copy + Sized,
{
if simplex_points.len() != D + 1 {
return Err(CoordinateConversionError::ConversionFailed {
coordinate_index: 0,
coordinate_value: format!("Expected {} points, got {}", D + 1, simplex_points.len()),
from_type: "point count",
to_type: "valid simplex",
});
}
let k = D + 1;
try_with_la_stack_matrix!(k, |matrix| {
for (i, point) in simplex_points.iter().enumerate() {
let coords = point.coords();
let coords_f64 = safe_coords_to_f64(coords)?;
for (j, &v) in coords_f64.iter().enumerate() {
matrix_set(&mut matrix, i, j, v);
}
matrix_set(&mut matrix, i, D, 1.0);
}
let base_tol = safe_scalar_to_f64(config.base_tolerance)?;
Ok(super::predicates::orientation_from_matrix(
&matrix, k, base_tol,
))
})
}
fn verify_insphere_consistency<T, const D: usize>(
simplex_points: &[Point<T, D>],
test_point: &Point<T, D>,
determinant_result: InSphere,
_config: &RobustPredicateConfig<T>,
) -> ConsistencyResult
where
T: ScalarSummable,
[T; D]: Copy + Sized,
{
super::predicates::insphere_distance(simplex_points, *test_point).map_or(
ConsistencyResult::Unverifiable,
|distance_result| match (determinant_result, distance_result) {
(InSphere::INSIDE, InSphere::INSIDE)
| (InSphere::OUTSIDE, InSphere::OUTSIDE)
| (InSphere::BOUNDARY, _)
| (_, InSphere::BOUNDARY) => ConsistencyResult::Consistent,
(InSphere::INSIDE, InSphere::OUTSIDE) | (InSphere::OUTSIDE, InSphere::INSIDE) => {
#[cfg(debug_assertions)]
tracing::warn!(
determinant_result = ?determinant_result,
distance_result = ?distance_result,
"Insphere consistency check failed"
);
ConsistencyResult::Inconsistent(InsphereConsistencyError::DirectContradiction {
determinant_result,
distance_result,
})
}
},
)
}
fn generate_perturbation_directions<T, const D: usize>() -> Vec<[T; D]>
where
T: CoordinateScalar,
{
let mut directions = Vec::new();
for i in 0..D {
let mut direction = [T::zero(); D];
direction[i] = T::one();
directions.push(direction);
direction[i] = -T::one();
directions.push(direction);
}
if D >= 2 {
let mut diag = [T::one(); D];
for item in diag.iter_mut().take(D) {
let d_value = cast(D).unwrap_or_else(T::one);
*item = *item / d_value;
}
directions.push(diag);
}
directions
}
fn apply_perturbation<T, const D: usize>(
point: &Point<T, D>,
direction: [T; D],
scale: T,
) -> Point<T, D>
where
T: CoordinateScalar,
[T; D]: Copy + Sized,
{
let mut coords = *point.coords();
for i in 0..D {
coords[i] = coords[i] + direction[i] * scale;
}
Point::new(coords)
}
fn geometric_deterministic_tie_breaking<T, const D: usize>(
simplex_points: &[Point<T, D>],
test_point: &Point<T, D>,
) -> InSphere
where
T: ScalarSummable,
[T; D]: Copy + Sized,
{
let perturbation_scale = T::from(1e-100)
.unwrap_or_else(|| T::default_tolerance() / T::from(1000).unwrap_or_else(T::one));
let mut perturbed_simplex: Vec<Point<T, D>> = Vec::with_capacity(simplex_points.len());
for (i, point) in simplex_points.iter().enumerate() {
let mut coords = *point.coords();
let perturbation_magnitude = perturbation_scale / T::from(i + 1).unwrap_or_else(T::one);
coords[0] = coords[0] + perturbation_magnitude;
perturbed_simplex.push(Point::new(coords));
}
let mut test_coords = *test_point.coords();
let test_perturbation =
perturbation_scale / T::from(simplex_points.len() + 1).unwrap_or_else(T::one);
test_coords[0] = test_coords[0] + test_perturbation;
let perturbed_test = Point::new(test_coords);
super::predicates::insphere_distance(&perturbed_simplex, perturbed_test).unwrap_or_else(|_| {
centroid_based_tie_breaking(simplex_points, test_point)
})
}
fn centroid_based_tie_breaking<T, const D: usize>(
simplex_points: &[Point<T, D>],
test_point: &Point<T, D>,
) -> InSphere
where
T: ScalarSummable,
[T; D]: Copy + Sized,
{
let mut centroid_coords = [T::zero(); D];
for point in simplex_points {
let coords = *point.coords();
for i in 0..D {
centroid_coords[i] = centroid_coords[i] + coords[i];
}
}
let num_points_scalar = T::from(simplex_points.len()).unwrap_or_else(T::one);
for coord in &mut centroid_coords {
*coord = *coord / num_points_scalar;
}
let test_coords = *test_point.coords();
let mut test_dist_sq = T::zero();
for i in 0..D {
let diff = test_coords[i] - centroid_coords[i];
test_dist_sq = test_dist_sq + diff * diff;
}
let mut avg_simplex_dist_sq = T::zero();
for point in simplex_points {
let coords = *point.coords();
let mut dist_sq = T::zero();
for i in 0..D {
let diff = coords[i] - centroid_coords[i];
dist_sq = dist_sq + diff * diff;
}
avg_simplex_dist_sq = avg_simplex_dist_sq + dist_sq;
}
avg_simplex_dist_sq = avg_simplex_dist_sq / num_points_scalar;
if test_dist_sq > avg_simplex_dist_sq {
InSphere::OUTSIDE
} else if test_dist_sq < avg_simplex_dist_sq {
InSphere::INSIDE
} else {
InSphere::BOUNDARY
}
}
pub mod config_presets {
use super::{CoordinateScalar, RobustPredicateConfig};
use num_traits::cast;
#[must_use]
pub fn general_triangulation<T: CoordinateScalar>() -> RobustPredicateConfig<T> {
RobustPredicateConfig {
base_tolerance: T::default_tolerance(),
relative_tolerance_factor: cast(1e-12).unwrap_or_else(T::default_tolerance),
max_refinement_iterations: 3,
exact_arithmetic_threshold: cast(1e-10).unwrap_or_else(T::default_tolerance),
perturbation_scale: cast(1e-10).unwrap_or_else(T::default_tolerance),
visibility_threshold_multiplier: cast(100.0)
.unwrap_or_else(|| T::from(100.0).unwrap_or_else(T::default_tolerance)),
}
}
#[must_use]
pub fn high_precision<T: CoordinateScalar>() -> RobustPredicateConfig<T> {
let base_tol = T::default_tolerance();
RobustPredicateConfig {
base_tolerance: base_tol / cast(100.0).unwrap_or_else(T::one),
relative_tolerance_factor: cast(1e-14).unwrap_or(base_tol),
max_refinement_iterations: 5,
exact_arithmetic_threshold: cast(1e-12).unwrap_or(base_tol),
perturbation_scale: cast(1e-12).unwrap_or(base_tol),
visibility_threshold_multiplier: cast(100.0)
.unwrap_or_else(|| T::from(100.0).unwrap_or_else(T::default_tolerance)),
}
}
#[must_use]
pub fn degenerate_robust<T: CoordinateScalar>() -> RobustPredicateConfig<T> {
let base_tol = T::default_tolerance();
RobustPredicateConfig {
base_tolerance: base_tol * cast(100.0).unwrap_or_else(T::one),
relative_tolerance_factor: cast(1e-10).unwrap_or(base_tol),
max_refinement_iterations: 2,
exact_arithmetic_threshold: cast(1e-8).unwrap_or(base_tol),
perturbation_scale: cast(1e-8).unwrap_or(base_tol),
visibility_threshold_multiplier: cast(200.0)
.unwrap_or_else(|| T::from(200.0).unwrap_or_else(T::default_tolerance)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::matrix::matrix_get;
use crate::geometry::point::Point;
use crate::geometry::predicates;
use approx::assert_relative_eq;
use num_traits::NumCast;
use rand::{RngExt, SeedableRng};
#[test]
fn test_robust_insphere_general() {
let points = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 1.0]),
];
let config = config_presets::general_triangulation();
let inside_point = Point::new([0.25, 0.25, 0.25]);
let result = robust_insphere(&points, &inside_point, &config).unwrap();
assert_eq!(result, InSphere::INSIDE);
let outside_point = Point::new([2.0, 2.0, 2.0]);
let result = robust_insphere(&points, &outside_point, &config).unwrap();
assert_eq!(result, InSphere::OUTSIDE);
}
#[test]
fn test_robust_orientation() {
let points = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 1.0]),
];
let config = config_presets::general_triangulation();
let result = robust_orientation(&points, &config).unwrap();
assert!(matches!(
result,
Orientation::POSITIVE | Orientation::NEGATIVE
));
}
#[test]
fn test_robust_orientation_positive_triangle_2d() {
let points = vec![
Point::new([0.0, 0.0]),
Point::new([1.0, 0.0]),
Point::new([0.0, 1.0]),
];
let config = config_presets::general_triangulation::<f64>();
let robust = robust_orientation(&points, &config).unwrap();
let reference = predicates::simplex_orientation(&points).unwrap();
assert_eq!(robust, Orientation::POSITIVE);
assert_eq!(robust, reference);
}
#[test]
fn test_robust_orientation_non_finite_base_tolerance_returns_error() {
let points = vec![
Point::new([0.0, 0.0]),
Point::new([1.0, 0.0]),
Point::new([0.0, 1.0]),
];
let config = RobustPredicateConfig {
base_tolerance: f64::NAN,
..config_presets::general_triangulation::<f64>()
};
let result = robust_orientation(&points, &config);
assert!(matches!(
result,
Err(CoordinateConversionError::NonFiniteValue { .. })
));
}
#[test]
fn test_robust_orientation_near_degenerate_2d_exact_sign() {
let eps = 2f64.powi(-50);
let points = vec![
Point::new([0.0, 0.0]),
Point::new([1.0, 0.0]),
Point::new([0.5, eps]),
];
let config = config_presets::general_triangulation::<f64>();
let result = robust_orientation(&points, &config).unwrap();
assert_eq!(
result,
Orientation::POSITIVE,
"near-degenerate 2D orientation should use exact sign and stay POSITIVE"
);
}
#[test]
fn test_robust_orientation_near_degenerate_3d_not_degenerate() {
let eps = 2f64.powi(-50);
let points = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, eps]),
];
let config = config_presets::general_triangulation::<f64>();
let result = robust_orientation(&points, &config).unwrap();
assert_ne!(
result,
Orientation::DEGENERATE,
"near-degenerate 3D orientation should not be DEGENERATE with exact sign"
);
}
#[test]
fn test_robust_insphere_near_cocircular_2d_exact_sign() {
let eps = 2f64.powi(-50);
let triangle = vec![
Point::new([0.0, 0.0]),
Point::new([1.0, 0.0]),
Point::new([0.0, 1.0]),
];
let radius = 0.5_f64.sqrt();
let inside_point = Point::new([0.5 + radius - eps, 0.5]);
let outside_point = Point::new([0.5 + radius + eps, 0.5]);
let config = config_presets::general_triangulation::<f64>();
assert_eq!(
robust_insphere(&triangle, &inside_point, &config).unwrap(),
InSphere::INSIDE,
"near-cocircular 2D point just inside boundary should be INSIDE"
);
assert_eq!(
robust_insphere(&triangle, &outside_point, &config).unwrap(),
InSphere::OUTSIDE,
"near-cocircular 2D point just outside boundary should be OUTSIDE"
);
}
#[test]
fn test_robust_insphere_near_cospherical_3d_exact_sign() {
let eps = 2f64.powi(-50);
let tetra = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 1.0]),
];
let radius = 0.75_f64.sqrt();
let inside_point = Point::new([0.5 + radius - eps, 0.5, 0.5]);
let outside_point = Point::new([0.5 + radius + eps, 0.5, 0.5]);
let config = config_presets::general_triangulation::<f64>();
assert_eq!(
robust_insphere(&tetra, &inside_point, &config).unwrap(),
InSphere::INSIDE,
"near-cospherical 3D point just inside boundary should be INSIDE"
);
assert_eq!(
robust_insphere(&tetra, &outside_point, &config).unwrap(),
InSphere::OUTSIDE,
"near-cospherical 3D point just outside boundary should be OUTSIDE"
);
}
#[test]
fn test_degenerate_case_handling() {
let points = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.5, 0.5, 1e-15]), ];
let config = config_presets::degenerate_robust();
let test_point = Point::new([0.25, 0.25, 1e-16]);
let result = robust_insphere(&points, &test_point, &config);
assert!(result.is_ok());
}
#[expect(clippy::too_many_lines)]
#[test]
fn test_verify_insphere_consistency_comprehensive() {
let config = config_presets::general_triangulation();
let points = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 1.0]),
];
let test_cases = [
(
Point::new([0.25, 0.25, 0.25]),
InSphere::INSIDE,
"inside point",
),
(
Point::new([2.0, 2.0, 2.0]),
InSphere::OUTSIDE,
"outside point",
),
(
Point::new([0.5, 0.5, 0.5]),
InSphere::BOUNDARY,
"boundary point",
),
];
for (test_point, result, description) in test_cases {
assert!(
verify_insphere_consistency(&points, &test_point, result, &config).is_consistent(),
"Failed for {description}"
);
}
let boundary_test_point = Point::new([0.3, 0.3, 0.3]);
for expected_result in [InSphere::INSIDE, InSphere::OUTSIDE, InSphere::BOUNDARY] {
if expected_result == InSphere::BOUNDARY {
assert!(
verify_insphere_consistency(
&points,
&boundary_test_point,
expected_result,
&config
)
.is_consistent()
);
}
}
let triangle_2d = vec![
Point::new([0.0, 0.0]),
Point::new([2.0, 0.0]),
Point::new([1.0, 2.0]),
];
let test_2d = Point::new([1.0, 0.5]);
assert!(
verify_insphere_consistency(&triangle_2d, &test_2d, InSphere::BOUNDARY, &config)
.is_consistent()
);
let edge_cases = [
(
vec![
Point::new([1e-10, 0.0, 0.0]),
Point::new([0.0, 1e-10, 0.0]),
Point::new([0.0, 0.0, 1e-10]),
Point::new([1e-10, 1e-10, 1e-10]),
],
Point::new([5e-11, 5e-11, 5e-11]),
"small coordinates",
),
(
vec![
Point::new([1e6, 0.0, 0.0]),
Point::new([0.0, 1e6, 0.0]),
Point::new([0.0, 0.0, 1e6]),
Point::new([1e6, 1e6, 1e6]),
],
Point::new([5e5, 5e5, 5e5]),
"large coordinates",
),
];
for (edge_points, edge_test, description) in edge_cases {
assert!(
verify_insphere_consistency(&edge_points, &edge_test, InSphere::BOUNDARY, &config)
.is_consistent(),
"Failed for edge case: {description}"
);
}
let error_cases = [
(
vec![Point::new([0.0, 0.0, 0.0]), Point::new([1.0, 0.0, 0.0])],
Point::new([0.5, 0.0, 0.0]),
"too few points",
),
(
vec![
Point::new([f64::NAN, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 1.0]),
],
Point::new([0.1, 0.1, 0.1]),
"NaN coordinates",
),
(
vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([2.0, 0.0, 0.0]),
Point::new([3.0, 0.0, 0.0]),
],
Point::new([1.5, 0.0, 0.0]),
"collinear points",
),
];
for (error_points, error_test, error_description) in error_cases {
let result = verify_insphere_consistency(
&error_points,
&error_test,
InSphere::BOUNDARY,
&config,
);
assert_eq!(
result,
ConsistencyResult::Unverifiable,
"Expected Unverifiable for: {error_description}"
);
assert!(
result.is_consistent(),
"Unverifiable should be considered consistent"
);
}
}
#[test]
fn test_consistency_result_display() {
assert_eq!(format!("{}", ConsistencyResult::Consistent), "Consistent");
assert_eq!(
format!(
"{}",
ConsistencyResult::Inconsistent(InsphereConsistencyError::DirectContradiction {
determinant_result: InSphere::INSIDE,
distance_result: InSphere::OUTSIDE,
})
),
"Insphere inconsistency: determinant=INSIDE, distance=OUTSIDE"
);
assert_eq!(
format!("{}", ConsistencyResult::Unverifiable),
"Unverifiable"
);
}
const PERIODIC_TWO_POW_52_I64: i64 = 4_503_599_627_370_496;
const PERIODIC_TWO_POW_52_F64: f64 = 4_503_599_627_370_496.0;
const PERIODIC_MAX_OFFSET_UNITS: i64 = 1_048_576;
const PERIODIC_IMAGE_JITTER_UNITS: i64 = 64;
const PERIODIC_FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const PERIODIC_FNV_PRIME: u64 = 0x0100_0000_01b3;
type PeriodicWitness3d = ([Point<f64, 3>; 4], Point<f64, 3>, InSphere, InSphere);
fn periodic_builder_perturb_units(canon_idx: usize, axis: usize) -> i64 {
let mut h = PERIODIC_FNV_OFFSET_BASIS;
h ^= u64::try_from(canon_idx).expect("canonical index fits in u64");
h = h.wrapping_mul(PERIODIC_FNV_PRIME);
h ^= u64::try_from(axis).expect("axis fits in u64");
h = h.wrapping_mul(PERIODIC_FNV_PRIME);
let span =
u64::try_from(2 * PERIODIC_MAX_OFFSET_UNITS + 1).expect("periodic span fits in u64");
i64::try_from(h % span).expect("residue fits in i64") - PERIODIC_MAX_OFFSET_UNITS
}
fn periodic_builder_image_jitter_units(canon_idx: usize, axis: usize, image_idx: usize) -> i64 {
let mut h = PERIODIC_FNV_OFFSET_BASIS;
h ^= u64::try_from(canon_idx).expect("canonical index fits in u64");
h = h.wrapping_mul(PERIODIC_FNV_PRIME);
h ^= u64::try_from(axis).expect("axis fits in u64");
h = h.wrapping_mul(PERIODIC_FNV_PRIME);
h ^= u64::try_from(image_idx).expect("image index fits in u64");
h = h.wrapping_mul(PERIODIC_FNV_PRIME);
let span = u64::try_from(2 * PERIODIC_IMAGE_JITTER_UNITS + 1)
.expect("periodic jitter span fits in u64");
i64::try_from(h % span).expect("residue fits in i64") - PERIODIC_IMAGE_JITTER_UNITS
}
fn periodic_3d_canonical_points() -> Vec<Point<f64, 3>> {
vec![
Point::new([0.1_f64, 0.2, 0.3]),
Point::new([0.4, 0.7, 0.1]),
Point::new([0.7, 0.3, 0.8]),
Point::new([0.2, 0.9, 0.5]),
Point::new([0.8, 0.6, 0.2]),
Point::new([0.5, 0.1, 0.7]),
Point::new([0.3, 0.5, 0.9]),
Point::new([0.6, 0.8, 0.4]),
Point::new([0.9, 0.2, 0.6]),
Point::new([0.0, 0.4, 0.1]),
Point::new([0.15, 0.65, 0.45]),
Point::new([0.75, 0.15, 0.85]),
Point::new([0.45, 0.55, 0.25]),
Point::new([0.85, 0.45, 0.65]),
]
}
fn periodic_3d_builder_style_expansion(
canonical_points: &[Point<f64, 3>],
) -> Vec<Point<f64, 3>> {
let canonical_f64: Vec<[f64; 3]> = canonical_points
.iter()
.enumerate()
.map(|(canon_idx, point)| {
let coords = point.coords();
let mut quantized = [0.0_f64; 3];
for axis in 0..3 {
let normalized = coords[axis].clamp(0.0, 1.0 - f64::EPSILON);
let scaled = (normalized * PERIODIC_TWO_POW_52_F64).floor();
let unit_index = <i64 as NumCast>::from(scaled)
.expect("scaled coordinate index fits in i64");
let min_off = -unit_index.min(PERIODIC_MAX_OFFSET_UNITS);
let max_off =
(PERIODIC_TWO_POW_52_I64 - 1 - unit_index).min(PERIODIC_MAX_OFFSET_UNITS);
let offset =
periodic_builder_perturb_units(canon_idx, axis).clamp(min_off, max_off);
let adjusted = <f64 as NumCast>::from(unit_index + offset)
.expect("adjusted index fits in f64");
quantized[axis] = adjusted / PERIODIC_TWO_POW_52_F64;
}
quantized
})
.collect();
let three_pow_d = 27_usize;
let zero_offset_idx = (three_pow_d - 1) / 2;
let mut expanded = Vec::with_capacity(canonical_points.len() * three_pow_d);
for image_idx in 0..three_pow_d {
let mut offset = [0_i8; 3];
for (axis, offset_val) in offset.iter_mut().enumerate() {
let axis_u32 = u32::try_from(axis).expect("axis index fits in u32");
let digit = (image_idx / 3_usize.pow(axis_u32)) % 3;
*offset_val = i8::try_from(digit).expect("digit fits in i8") - 1;
}
let is_canonical = image_idx == zero_offset_idx;
for (canon_idx, quantized) in canonical_f64.iter().enumerate() {
let mut image_coords = [0.0_f64; 3];
for axis in 0..3 {
let shift = <f64 as std::convert::From<i8>>::from(offset[axis]);
let jitter = if is_canonical {
0.0
} else {
let jitter_units =
periodic_builder_image_jitter_units(canon_idx, axis, image_idx);
let jitter_as_f64 =
<f64 as NumCast>::from(jitter_units).expect("jitter units fit in f64");
jitter_as_f64 / PERIODIC_TWO_POW_52_F64
};
image_coords[axis] = quantized[axis] + shift + jitter;
}
expanded.push(Point::new(image_coords));
}
}
expanded
}
fn find_periodic_3d_inconsistency_witness(
expanded: &[Point<f64, 3>],
config: &RobustPredicateConfig<f64>,
seed: u64,
sample_budget: usize,
) -> Option<PeriodicWitness3d> {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let n = expanded.len();
if n < 5 {
return None;
}
for _ in 0..sample_budget {
let i0 = rng.random_range(0..n);
let mut i1 = rng.random_range(0..n);
while i1 == i0 {
i1 = rng.random_range(0..n);
}
let mut i2 = rng.random_range(0..n);
while i2 == i0 || i2 == i1 {
i2 = rng.random_range(0..n);
}
let mut i3 = rng.random_range(0..n);
while i3 == i0 || i3 == i1 || i3 == i2 {
i3 = rng.random_range(0..n);
}
let mut it = rng.random_range(0..n);
while it == i0 || it == i1 || it == i2 || it == i3 {
it = rng.random_range(0..n);
}
let simplex = [expanded[i0], expanded[i1], expanded[i2], expanded[i3]];
let test_point = expanded[it];
let det_result = adaptive_tolerance_insphere(&simplex, &test_point, config);
let dist_result = predicates::insphere_distance(&simplex, test_point);
if let (Ok(det), Ok(dist)) = (det_result, dist_result)
&& matches!(
(det, dist),
(InSphere::INSIDE, InSphere::OUTSIDE) | (InSphere::OUTSIDE, InSphere::INSIDE)
)
{
return Some((simplex, test_point, det, dist));
}
}
None
}
#[test]
#[ignore = "stress test; run explicitly with --ignored"]
fn test_periodic_3d_inconsistency_witness_search_seeded() {
let config = config_presets::general_triangulation::<f64>();
let canonical_points = periodic_3d_canonical_points();
let expanded = periodic_3d_builder_style_expansion(&canonical_points);
let witness =
find_periodic_3d_inconsistency_witness(&expanded, &config, 0x2100_0003, 200_000);
if let Some((simplex, test_point, det, dist)) = witness {
panic!(
"Found periodic-3D determinant-vs-distance inconsistency: determinant={det:?}, distance={dist:?}, simplex={simplex:?}, test_point={test_point:?}"
);
}
}
#[test]
fn test_robust_predicates_dimensional_coverage() {
let config = config_presets::general_triangulation();
let triangle_2d = vec![
Point::new([0.0, 0.0]),
Point::new([1.0, 0.0]),
Point::new([0.5, 1.0]),
];
let test_2d = Point::new([0.5, 0.3]);
assert!(
robust_insphere(&triangle_2d, &test_2d, &config).is_ok(),
"2D insphere should work"
);
assert!(
robust_orientation(&triangle_2d, &config).is_ok(),
"2D orientation should work"
);
let tetrahedron_3d = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 1.0]),
];
let test_3d = Point::new([0.25, 0.25, 0.25]);
assert!(
robust_insphere(&tetrahedron_3d, &test_3d, &config).is_ok(),
"3D insphere should work"
);
assert!(
robust_orientation(&tetrahedron_3d, &config).is_ok(),
"3D orientation should work"
);
let simplex_4d = vec![
Point::new([0.0, 0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 0.0, 1.0]),
];
let test_4d = Point::new([0.2, 0.2, 0.2, 0.2]);
assert!(
robust_insphere(&simplex_4d, &test_4d, &config).is_ok(),
"4D insphere should work"
);
assert!(
robust_orientation(&simplex_4d, &config).is_ok(),
"4D orientation should work"
);
let simplex_5d = vec![
Point::new([0.0, 0.0, 0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 1.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 0.0, 0.0, 1.0]),
];
let test_5d = Point::new([0.15, 0.15, 0.15, 0.15, 0.15]);
assert!(
robust_insphere(&simplex_5d, &test_5d, &config).is_ok(),
"5D insphere should work"
);
assert!(
robust_orientation(&simplex_5d, &config).is_ok(),
"5D orientation should work"
);
let too_few_2d = vec![Point::new([0.0, 0.0])];
let insphere_2d_err = robust_insphere(&too_few_2d, &test_2d, &config);
let orientation_2d_err = robust_orientation(&too_few_2d, &config);
assert!(
insphere_2d_err.is_err() || orientation_2d_err.is_err(),
"2D should fail with 1 point"
);
let too_few_3d = vec![Point::new([0.0, 0.0, 0.0]), Point::new([1.0, 0.0, 0.0])];
let insphere_3d_err = robust_insphere(&too_few_3d, &test_3d, &config);
let orientation_3d_err = robust_orientation(&too_few_3d, &config);
assert!(
insphere_3d_err.is_err() || orientation_3d_err.is_err(),
"3D should fail with 2 points"
);
let too_few_4d = vec![
Point::new([0.0, 0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0, 0.0]),
];
let insphere_4d_err = robust_insphere(&too_few_4d, &test_4d, &config);
assert!(insphere_4d_err.is_err(), "4D should fail with 3 points");
}
#[test]
fn test_symbolic_perturbation_fallback() {
let config = RobustPredicateConfig {
base_tolerance: 1e-12,
relative_tolerance_factor: 1e-15,
max_refinement_iterations: 1,
exact_arithmetic_threshold: 1e-8,
perturbation_scale: 1e-15, visibility_threshold_multiplier: 100.0,
};
let nearly_coplanar_points = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.5, 1.0, 0.0]),
Point::new([0.5, 0.5, 1e-16]), ];
let boundary_test_point = Point::new([0.5, 0.5, 5e-17]);
let result = robust_insphere(&nearly_coplanar_points, &boundary_test_point, &config);
assert!(result.is_ok());
let insphere_result = result.unwrap();
assert!(matches!(
insphere_result,
InSphere::INSIDE | InSphere::BOUNDARY | InSphere::OUTSIDE
));
}
#[test]
fn test_matrix_conditioning_edge_cases() {
let scale_small = with_la_stack_matrix!(3, |m| {
matrix_set(&mut m, 0, 0, 1e-100);
matrix_set(&mut m, 1, 1, 1e-99);
matrix_set(&mut m, 2, 2, 1e-98);
let mut scale_factor = 1.0_f64;
for i in 0..3 {
let mut max_element = 0.0_f64;
for j in 0..3 {
max_element = max_element.max(matrix_get(&m, i, j).abs());
}
if max_element > 1e-100 {
for j in 0..3 {
let v = matrix_get(&m, i, j) / max_element;
matrix_set(&mut m, i, j, v);
}
scale_factor *= max_element;
}
}
for i in 0..3 {
for j in 0..3 {
assert!(matrix_get(&m, i, j).is_finite());
}
}
scale_factor
});
assert!(scale_small.is_finite());
let scale_mixed = with_la_stack_matrix!(3, |m| {
matrix_set(&mut m, 0, 0, 1e10);
matrix_set(&mut m, 0, 1, 1e-10);
matrix_set(&mut m, 1, 0, 1e5);
matrix_set(&mut m, 1, 1, 1e-5);
matrix_set(&mut m, 2, 2, 1.0);
let mut scale_factor = 1.0_f64;
for i in 0..3 {
let mut max_element = 0.0_f64;
for j in 0..3 {
max_element = max_element.max(matrix_get(&m, i, j).abs());
}
if max_element > 1e-100 {
for j in 0..3 {
let v = matrix_get(&m, i, j) / max_element;
matrix_set(&mut m, i, j, v);
}
scale_factor *= max_element;
}
}
for i in 0..3 {
for j in 0..3 {
assert!(matrix_get(&m, i, j).is_finite());
}
}
scale_factor
});
assert!(scale_mixed.is_finite() && scale_mixed > 0.0);
let scale_zero = with_la_stack_matrix!(3, |m| {
matrix_set(&mut m, 0, 0, 1.0);
matrix_set(&mut m, 1, 1, 0.0); matrix_set(&mut m, 2, 2, 2.0);
let mut scale_factor = 1.0_f64;
for i in 0..3 {
let mut max_element = 0.0_f64;
for j in 0..3 {
max_element = max_element.max(matrix_get(&m, i, j).abs());
}
if max_element > 1e-100 {
for j in 0..3 {
let v = matrix_get(&m, i, j) / max_element;
matrix_set(&mut m, i, j, v);
}
scale_factor *= max_element;
}
}
for i in 0..3 {
for j in 0..3 {
assert!(matrix_get(&m, i, j).is_finite());
}
}
scale_factor
});
assert!(scale_zero.is_finite());
}
#[test]
fn test_tie_breaking_comprehensive() {
let degenerate_config = RobustPredicateConfig {
base_tolerance: 1e-15_f64,
relative_tolerance_factor: 1e-15_f64,
max_refinement_iterations: 1,
exact_arithmetic_threshold: 1e-18_f64,
perturbation_scale: 1e-18_f64,
visibility_threshold_multiplier: 100.0,
};
let triangle_2d = vec![
Point::new([0.0, 0.0]),
Point::new([1.0, 0.0]),
Point::new([0.5, 1e-15]), ];
let test_2d = Point::new([0.5, 1e-16]);
let result_2d = robust_insphere(&triangle_2d, &test_2d, °enerate_config);
assert!(result_2d.is_ok(), "2D tie-breaking should work");
let coplanar_3d = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.5, 0.5, 0.0]), ];
let test_3d = Point::new([0.25, 0.25, 0.0]);
let result_3d = robust_insphere(&coplanar_3d, &test_3d, °enerate_config);
assert!(
result_3d.is_ok(),
"3D tie-breaking should handle coplanar points"
);
let simplex_4d = vec![
Point::new([0.0, 0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 1.0, 0.0]),
Point::new([1e-14, 1e-14, 1e-14, 1.0]), ];
let test_4d = Point::new([0.2, 0.2, 0.2, 1e-15]);
let result_4d = robust_insphere(&simplex_4d, &test_4d, °enerate_config);
assert!(result_4d.is_ok(), "4D tie-breaking should work");
let simplex_5d = vec![
Point::new([0.0, 0.0, 0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 1.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 0.0, 1.0, 0.0]),
Point::new([1e-12, 1e-12, 1e-12, 1e-12, 1.0]), ];
let test_5d = Point::new([0.1, 0.1, 0.1, 0.1, 1e-13]);
let result_5d = robust_insphere(&simplex_5d, &test_5d, °enerate_config);
assert!(result_5d.is_ok(), "5D tie-breaking should work");
let result_3d_repeat = robust_insphere(&coplanar_3d, &test_3d, °enerate_config);
assert_eq!(
result_3d, result_3d_repeat,
"Tie-breaking should be deterministic"
);
let config = config_presets::general_triangulation::<f64>();
let extreme_cases = [
(
vec![
Point::new([1e-100, 0.0, 0.0]),
Point::new([0.0, 1e-100, 0.0]),
Point::new([0.0, 0.0, 1e-100]),
Point::new([1e-101, 1e-101, 1e-101]),
],
Point::new([5e-102, 5e-102, 5e-102]),
"tiny coordinates",
),
(
vec![
Point::new([1e50, 0.0, 0.0]),
Point::new([0.0, 1e50, 0.0]),
Point::new([0.0, 0.0, 1e50]),
Point::new([1e49, 1e49, 1e49]),
],
Point::new([5e48, 5e48, 5e48]),
"huge coordinates",
),
];
for (simplex, test_point, description) in extreme_cases {
let result = robust_insphere(&simplex, &test_point, &config);
assert!(result.is_ok(), "Should handle {description}");
}
let regular_tetrahedron = vec![
Point::new([1.0, 1.0, 1.0]),
Point::new([1.0, -1.0, -1.0]),
Point::new([-1.0, 1.0, -1.0]),
Point::new([-1.0, -1.0, 1.0]),
];
let clearly_inside = Point::new([0.0, 0.0, 0.0]);
let clearly_outside = Point::new([5.0, 5.0, 5.0]);
assert_eq!(
robust_insphere(®ular_tetrahedron, &clearly_inside, &config).unwrap(),
InSphere::INSIDE,
"Center should be inside"
);
assert_eq!(
robust_insphere(®ular_tetrahedron, &clearly_outside, &config).unwrap(),
InSphere::OUTSIDE,
"Far point should be outside"
);
}
#[test]
fn test_config_fallback_values() {
let general_config = config_presets::general_triangulation::<f64>();
assert!(general_config.base_tolerance > 0.0);
assert!(general_config.relative_tolerance_factor > 0.0);
assert!(general_config.exact_arithmetic_threshold > 0.0);
assert!(general_config.perturbation_scale > 0.0);
assert_eq!(general_config.max_refinement_iterations, 3);
let high_precision_config = config_presets::high_precision::<f64>();
assert!(high_precision_config.base_tolerance > 0.0);
assert!(high_precision_config.base_tolerance < general_config.base_tolerance);
assert_eq!(high_precision_config.max_refinement_iterations, 5);
let degenerate_config = config_presets::degenerate_robust::<f64>();
assert!(degenerate_config.base_tolerance > 0.0);
assert!(degenerate_config.base_tolerance > general_config.base_tolerance);
assert_eq!(degenerate_config.max_refinement_iterations, 2);
let f32_config = config_presets::general_triangulation::<f32>();
assert!(f32_config.base_tolerance > 0.0);
assert!(f32_config.relative_tolerance_factor > 0.0);
}
#[test]
fn test_deterministic_tie_breaking() {
let config = config_presets::general_triangulation();
let identical_points = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.5, 1.0, 0.0]),
Point::new([0.5, 0.5, 1.0]),
];
let identical_test = Point::new([0.0, 0.0, 0.0]);
let result = robust_insphere(&identical_points, &identical_test, &config);
assert!(result.is_ok());
let ordered_points = vec![
Point::new([1.0, 2.0, 3.0]),
Point::new([4.0, 5.0, 6.0]),
Point::new([7.0, 8.0, 9.0]),
Point::new([10.0, 11.0, 12.0]),
];
let smaller_test = Point::new([0.0, 1.0, 2.0]);
let result_smaller = robust_insphere(&ordered_points, &smaller_test, &config);
assert!(result_smaller.is_ok());
let larger_test = Point::new([15.0, 16.0, 17.0]);
let result_larger = robust_insphere(&ordered_points, &larger_test, &config);
assert!(result_larger.is_ok());
}
#[test]
fn test_adaptive_tolerance_computation() {
let config = config_presets::general_triangulation::<f64>();
let tolerance_small = with_la_stack_matrix!(2, |m| {
matrix_set(&mut m, 0, 0, 1.0);
matrix_set(&mut m, 0, 1, 2.0);
matrix_set(&mut m, 1, 0, 3.0);
matrix_set(&mut m, 1, 1, 4.0);
crate::geometry::matrix::adaptive_tolerance(&m, config.base_tolerance)
});
assert!(tolerance_small > 0.0);
let tolerance_large = with_la_stack_matrix!(5, |m| {
for i in 0..5 {
for j in 0..5 {
let sum_f64 = num_traits::cast::<usize, f64>(i + j).unwrap_or(0.0);
matrix_set(&mut m, i, j, sum_f64 * 1000.0);
}
}
crate::geometry::matrix::adaptive_tolerance(&m, config.base_tolerance)
});
assert!(tolerance_large > 0.0);
assert!(tolerance_large > tolerance_small);
let tolerance_tiny = with_la_stack_matrix!(3, |m| {
for i in 0..3 {
for j in 0..3 {
matrix_set(&mut m, i, j, 1e-10);
}
}
crate::geometry::matrix::adaptive_tolerance(&m, config.base_tolerance)
});
assert!(tolerance_tiny > 0.0);
}
#[test]
fn test_perturbation_direction_generation() {
let directions_1d = generate_perturbation_directions::<f64, 1>();
assert_eq!(directions_1d.len(), 2); assert_relative_eq!(directions_1d[0][0], 1.0);
assert_relative_eq!(directions_1d[1][0], -1.0);
let directions_2d = generate_perturbation_directions::<f64, 2>();
assert_eq!(directions_2d.len(), 5);
let directions_3d = generate_perturbation_directions::<f64, 3>();
assert_eq!(directions_3d.len(), 7);
let diag_3d = directions_3d.last().unwrap();
for &component in diag_3d {
assert!((component - 1.0 / 3.0).abs() < 1e-10);
}
let directions_4d = generate_perturbation_directions::<f64, 4>();
assert_eq!(directions_4d.len(), 9); }
#[test]
fn test_apply_perturbation() {
let original_point = Point::new([1.0, 2.0, 3.0]);
let direction = [0.1, -0.1, 0.2];
let scale = 0.001;
let perturbed = apply_perturbation(&original_point, direction, scale);
let perturbed_coords = *perturbed.coords();
assert_relative_eq!(
perturbed_coords[0],
0.1f64.mul_add(0.001, 1.0),
epsilon = 1e-15
);
assert_relative_eq!(
perturbed_coords[1],
0.1f64.mul_add(-0.001, 2.0),
epsilon = 1e-15
);
assert_relative_eq!(
perturbed_coords[2],
0.2f64.mul_add(0.001, 3.0),
epsilon = 1e-15
);
let zero_direction = [0.0, 0.0, 0.0];
let unperturbed = apply_perturbation(&original_point, zero_direction, 1.0);
let unperturbed_coords = *unperturbed.coords();
assert_relative_eq!(unperturbed_coords.as_slice(), [1.0, 2.0, 3.0].as_slice());
}
#[test]
fn test_consistency_check_fallback_branch() {
let strict_config = RobustPredicateConfig {
base_tolerance: 1e-20, relative_tolerance_factor: 1e-20,
max_refinement_iterations: 1,
exact_arithmetic_threshold: 1e-20,
perturbation_scale: 1e-20,
visibility_threshold_multiplier: 100.0,
};
let challenging_points = vec![
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 1.0]),
Point::new([1e-10, 1e-10, 1e-10]), ];
let test_point = Point::new([0.5, 0.5, 0.5]);
let result = robust_insphere(&challenging_points, &test_point, &strict_config);
assert!(result.is_ok());
let insphere_result = result.unwrap();
assert!(matches!(
insphere_result,
InSphere::INSIDE | InSphere::BOUNDARY | InSphere::OUTSIDE
));
}
#[test]
fn test_nan_tolerance_returns_error() {
let problematic_config = RobustPredicateConfig {
base_tolerance: f64::NAN,
relative_tolerance_factor: 1e-12,
max_refinement_iterations: 3,
exact_arithmetic_threshold: 1e-10,
perturbation_scale: 1e-10,
visibility_threshold_multiplier: 100.0,
};
let points = vec![
Point::new([0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 1.0]),
];
let test_point = Point::new([0.25, 0.25, 0.25]);
let result = robust_insphere(&points, &test_point, &problematic_config);
assert!(
result.is_err(),
"NaN tolerance should produce an error, not silently fall through"
);
let ill_conditioned_points = vec![
Point::new([1e-15, 0.0, 0.0]),
Point::new([0.0, 1e15, 0.0]),
Point::new([0.0, 0.0, 1e-8]),
Point::new([1e8, 1e-12, 1e4]),
];
let normal_config = config_presets::general_triangulation::<f64>();
let ill_test_point = Point::new([1e-10, 1e10, 1e-5]);
let ill_result = robust_insphere(&ill_conditioned_points, &ill_test_point, &normal_config);
assert!(ill_result.is_ok());
}
#[test]
fn test_build_matrices_edge_cases() {
let zero_points = [
Point::new([0.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 0.0]),
];
let zero_test = Point::new([0.0, 0.0, 0.0]);
let all_finite_insphere_3d = with_la_stack_matrix!(5, |matrix| {
for (i, point) in zero_points.iter().enumerate() {
let coords = point.coords();
for (j, &v) in coords.iter().enumerate() {
matrix_set(&mut matrix, i, j, v);
}
matrix_set(&mut matrix, i, 3, squared_norm(coords));
matrix_set(&mut matrix, i, 4, 1.0);
}
let test_coords = zero_test.coords();
for (j, &v) in test_coords.iter().enumerate() {
matrix_set(&mut matrix, 4, j, v);
}
matrix_set(&mut matrix, 4, 3, squared_norm(test_coords));
matrix_set(&mut matrix, 4, 4, 1.0);
let mut ok = true;
for r in 0..5 {
for c in 0..5 {
ok &= matrix_get(&matrix, r, c).is_finite();
}
}
ok
});
assert!(all_finite_insphere_3d);
let all_finite_orientation_3d = with_la_stack_matrix!(4, |matrix| {
for (i, point) in zero_points.iter().enumerate() {
let coords = point.coords();
for (j, &v) in coords.iter().enumerate() {
matrix_set(&mut matrix, i, j, v);
}
matrix_set(&mut matrix, i, 3, 1.0);
}
let mut ok = true;
for r in 0..4 {
for c in 0..4 {
ok &= matrix_get(&matrix, r, c).is_finite();
}
}
ok
});
assert!(all_finite_orientation_3d);
let large_points = [
Point::new([1e100, 0.0]),
Point::new([0.0, 1e100]),
Point::new([1e100, 1e100]),
];
let large_test = Point::new([5e99, 5e99]);
let all_finite_insphere_2d = with_la_stack_matrix!(4, |matrix| {
for (i, point) in large_points.iter().enumerate() {
let coords = point.coords();
for (j, &v) in coords.iter().enumerate() {
matrix_set(&mut matrix, i, j, v);
}
matrix_set(&mut matrix, i, 2, squared_norm(coords));
matrix_set(&mut matrix, i, 3, 1.0);
}
let test_coords = large_test.coords();
for (j, &v) in test_coords.iter().enumerate() {
matrix_set(&mut matrix, 3, j, v);
}
matrix_set(&mut matrix, 3, 2, squared_norm(test_coords));
matrix_set(&mut matrix, 3, 3, 1.0);
let mut ok = true;
for r in 0..4 {
for c in 0..4 {
ok &= matrix_get(&matrix, r, c).is_finite();
}
}
ok
});
assert!(all_finite_insphere_2d);
}
#[test]
fn test_symbolic_perturbation_insphere_via_6d() {
let simplex: Vec<Point<f64, 6>> = vec![
Point::new([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
Point::new([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
Point::new([0.0, 1.0, 0.0, 0.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 1.0, 0.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 0.0, 1.0, 0.0, 0.0]),
Point::new([0.0, 0.0, 0.0, 0.0, 1.0, 0.0]),
Point::new([0.0, 0.0, 0.0, 0.0, 0.0, 1.0]),
];
let config = config_presets::general_triangulation::<f64>();
let far = Point::new([5.0, 5.0, 5.0, 5.0, 5.0, 5.0]);
let result = robust_insphere(&simplex, &far, &config).unwrap();
assert_eq!(result, InSphere::OUTSIDE);
let near = Point::new([0.05, 0.05, 0.05, 0.05, 0.05, 0.05]);
let result = robust_insphere(&simplex, &near, &config).unwrap();
assert_eq!(result, InSphere::INSIDE);
}
#[test]
fn test_config_preset_high_precision() {
let config = config_presets::high_precision::<f64>();
assert_eq!(config.max_refinement_iterations, 5);
assert!(
config.base_tolerance < f64::default_tolerance(),
"high_precision base_tolerance should be stricter than default"
);
}
}