#![forbid(unsafe_code)]
use num_traits::{Float, Zero};
use ordered_float::OrderedFloat;
use serde::{Serialize, de::DeserializeOwned};
use std::{
fmt::Debug,
hash::{Hash, Hasher},
iter::Sum,
ops::{AddAssign, SubAssign},
};
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
pub enum CoordinateConversionError {
#[error(
"Failed to convert coordinate at index {coordinate_index} from {from_type} to {to_type}: {coordinate_value}"
)]
ConversionFailed {
coordinate_index: usize,
coordinate_value: String,
from_type: &'static str,
to_type: &'static str,
},
#[error(
"Non-finite value (NaN or infinity) at coordinate index {coordinate_index}: {coordinate_value}"
)]
NonFiniteValue {
coordinate_index: usize,
coordinate_value: String,
},
#[error("Insphere consistency check failed: {details}")]
InsphereInconsistency {
simplex_points: String,
test_point: String,
details: String,
},
}
impl From<crate::geometry::matrix::StackMatrixDispatchError> for CoordinateConversionError {
fn from(source: crate::geometry::matrix::StackMatrixDispatchError) -> Self {
match source {
crate::geometry::matrix::StackMatrixDispatchError::UnsupportedDim { k, max } => {
Self::ConversionFailed {
coordinate_index: 0,
coordinate_value: format!("unsupported stack matrix size: {k} (max {max})"),
from_type: "matrix dimension",
to_type: "stack matrix",
}
}
crate::geometry::matrix::StackMatrixDispatchError::La(source) => {
Self::ConversionFailed {
coordinate_index: 0,
coordinate_value: source.to_string(),
from_type: "la-stack",
to_type: "linear algebra",
}
}
}
}
}
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
pub enum CoordinateValidationError {
#[error(
"Invalid coordinate at index {coordinate_index} in dimension {dimension}: {coordinate_value}"
)]
InvalidCoordinate {
coordinate_index: usize,
coordinate_value: String,
dimension: usize,
},
}
pub const DEFAULT_TOLERANCE_F32: f32 = 1e-6;
pub const DEFAULT_TOLERANCE_F64: f64 = 1e-15;
pub trait FiniteCheck {
fn is_finite_generic(&self) -> bool;
}
macro_rules! impl_finite_check {
(float: $($t:ty),*) => {
$(
impl FiniteCheck for $t {
#[inline(always)]
fn is_finite_generic(&self) -> bool {
self.is_finite()
}
}
)*
};
}
impl_finite_check!(float: f32, f64);
pub trait OrderedEq {
fn ordered_eq(&self, other: &Self) -> bool;
}
macro_rules! impl_ordered_eq {
(float: $($t:ty),*) => {
$(
impl OrderedEq for $t {
#[inline(always)]
fn ordered_eq(&self, other: &Self) -> bool {
OrderedFloat(*self) == OrderedFloat(*other)
}
}
)*
};
}
impl_ordered_eq!(float: f32, f64);
pub trait OrderedCmp {
fn ordered_partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering>;
}
macro_rules! impl_ordered_cmp {
(float: $($t:ty),*) => {
$(
impl OrderedCmp for $t {
#[inline(always)]
fn ordered_partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(OrderedFloat(*self).cmp(&OrderedFloat(*other)))
}
}
)*
};
}
impl_ordered_cmp!(float: f32, f64);
pub trait HashCoordinate {
fn hash_scalar<H: Hasher>(&self, state: &mut H);
}
macro_rules! impl_hash_coordinate {
(float: $($t:ty),*) => {
$(
impl HashCoordinate for $t {
#[inline(always)]
fn hash_scalar<H: Hasher>(&self, state: &mut H) {
OrderedFloat(*self).hash(state);
}
}
)*
};
}
impl_hash_coordinate!(float: f32, f64);
pub trait CoordinateScalar:
Float
+ Zero
+ OrderedEq
+ OrderedCmp
+ HashCoordinate
+ FiniteCheck
+ Default
+ Debug
+ Serialize
+ DeserializeOwned
{
fn default_tolerance() -> Self;
fn mantissa_digits() -> u32;
}
pub trait ScalarSummable: CoordinateScalar + Sum {}
pub trait ScalarAccumulative: CoordinateScalar + AddAssign + SubAssign + Sum {}
impl<T> ScalarSummable for T where T: CoordinateScalar + Sum {}
impl<T> ScalarAccumulative for T where T: CoordinateScalar + AddAssign + SubAssign + Sum {}
impl CoordinateScalar for f32 {
fn default_tolerance() -> Self {
DEFAULT_TOLERANCE_F32
}
fn mantissa_digits() -> u32 {
Self::MANTISSA_DIGITS
}
}
impl CoordinateScalar for f64 {
fn default_tolerance() -> Self {
DEFAULT_TOLERANCE_F64
}
fn mantissa_digits() -> u32 {
Self::MANTISSA_DIGITS
}
}
pub trait Coordinate<T, const D: usize>
where
T: CoordinateScalar,
Self: Copy
+ Clone
+ Default
+ Debug
+ PartialEq
+ Eq
+ Hash
+ PartialOrd
+ Serialize
+ DeserializeOwned
+ Sized,
{
#[must_use]
fn dim(&self) -> usize {
D
}
fn new(coords: [T; D]) -> Self;
#[must_use]
fn to_array(&self) -> [T; D];
#[must_use]
fn get(&self, index: usize) -> Option<T>;
#[must_use]
fn origin() -> Self
where
T: Zero,
{
Self::new([T::zero(); D])
}
fn validate(&self) -> Result<(), CoordinateValidationError>;
fn hash_coordinate<H: Hasher>(&self, state: &mut H);
#[must_use]
fn ordered_equals(&self, other: &Self) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::point::Point;
use approx::assert_relative_eq;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
#[test]
fn coordinate_trait_basic_functionality() {
let coord: Point<f64, 3> = Point::new([1.0, 2.0, 3.0]);
assert_eq!(coord.dim(), 3);
assert_relative_eq!(
coord.to_array().as_slice(),
[1.0, 2.0, 3.0].as_slice(),
epsilon = DEFAULT_TOLERANCE_F64
);
assert_relative_eq!(coord.get(0).unwrap(), 1.0, epsilon = DEFAULT_TOLERANCE_F64);
assert_relative_eq!(coord.get(1).unwrap(), 2.0, epsilon = DEFAULT_TOLERANCE_F64);
assert_relative_eq!(coord.get(2).unwrap(), 3.0, epsilon = DEFAULT_TOLERANCE_F64);
assert_eq!(coord.get(3), None);
assert_eq!(coord.get(10), None);
let coord_f32: Point<f32, 3> = Point::new([1.5f32, 2.5f32, 3.5f32]);
assert_eq!(coord_f32.dim(), 3);
assert_relative_eq!(
coord_f32.to_array().as_slice(),
[1.5f32, 2.5f32, 3.5f32].as_slice(),
epsilon = DEFAULT_TOLERANCE_F32
);
assert!(coord_f32.validate().is_ok());
let coord_single: Point<f64, 1> = Point::new([42.0]);
assert_eq!(coord_single.dim(), 1);
assert_relative_eq!(
coord_single.get(0).unwrap(),
42.0,
epsilon = DEFAULT_TOLERANCE_F64
);
assert_eq!(coord_single.get(1), None);
let coord_zero: Point<f64, 0> = Point::new([]);
assert_eq!(coord_zero.dim(), 0);
assert_eq!(coord_zero.to_array().len(), 0);
assert_eq!(coord_zero.get(0), None);
assert!(coord_zero.validate().is_ok());
let coord_large: Point<f64, 10> =
Point::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
assert_eq!(coord_large.dim(), 10);
assert_eq!(coord_large.get(10), None);
assert!(coord_large.validate().is_ok());
}
#[test]
fn coordinate_trait_new() {
let coord1: Point<f64, 2> = Coordinate::new([5.0, 6.0]);
assert_relative_eq!(
coord1.to_array().as_slice(),
[5.0, 6.0].as_slice(),
epsilon = DEFAULT_TOLERANCE_F64
);
let coord2: Point<f64, 2> = Coordinate::new([5.0, 6.0]);
assert_relative_eq!(
coord2.to_array().as_slice(),
[5.0, 6.0].as_slice(),
epsilon = DEFAULT_TOLERANCE_F64
);
assert_eq!(coord1, coord2);
}
#[test]
fn coordinate_trait_origin() {
let origin_single: Point<f64, 1> = Point::origin();
assert_relative_eq!(
origin_single.to_array().as_slice(),
[0.0].as_slice(),
epsilon = DEFAULT_TOLERANCE_F64
);
let origin_triple: Point<f64, 3> = Point::origin();
assert_relative_eq!(
origin_triple.to_array().as_slice(),
[0.0, 0.0, 0.0].as_slice(),
epsilon = DEFAULT_TOLERANCE_F64
);
let origin_f32: Point<f32, 3> = Point::origin();
assert_relative_eq!(
origin_f32.to_array().as_slice(),
[0.0f32, 0.0f32, 0.0f32].as_slice(),
epsilon = DEFAULT_TOLERANCE_F32
);
let origin_zero: Point<f64, 0> = Point::origin();
assert_eq!(origin_zero.to_array().len(), 0);
let origin_large: Point<f64, 10> = Point::origin();
assert_relative_eq!(
origin_large.to_array().as_slice(),
[0.0; 10].as_slice(),
epsilon = DEFAULT_TOLERANCE_F64
);
}
#[test]
fn coordinate_trait_validate_comprehensive() {
let valid_cases = [
([1.0, 2.0, 3.0], "positive values"),
([-1.0, -2.0, -3.0], "negative values"),
([0.0, 0.0, 0.0], "zeros"),
([1e10, 2e10, 3e10], "large values"),
([1e-10, 2e-10, 3e-10], "small values"),
];
for &(coords, description) in &valid_cases {
let coord: Point<f64, 3> = Point::new(coords);
assert!(coord.validate().is_ok(), "Valid case failed: {description}");
}
let invalid_cases = [
([f64::NAN, 2.0, 3.0], 0, 3, "NaN at start"),
([1.0, f64::NAN, 3.0], 1, 3, "NaN in middle"),
([1.0, 2.0, f64::NAN], 2, 3, "NaN at end"),
([f64::INFINITY, 2.0, 3.0], 0, 3, "positive infinity"),
([1.0, f64::NEG_INFINITY, 3.0], 1, 3, "negative infinity"),
];
for &(coords, expected_index, expected_dim, description) in &invalid_cases {
let coord: Point<f64, 3> = Point::new(coords);
let result = coord.validate();
assert!(result.is_err(), "Invalid case should fail: {description}");
if let Err(CoordinateValidationError::InvalidCoordinate {
coordinate_index,
dimension,
..
}) = result
{
assert_eq!(
coordinate_index, expected_index,
"Wrong index for: {description}"
);
assert_eq!(
dimension, expected_dim,
"Wrong dimension for: {description}"
);
}
}
let multi_invalid: Point<f64, 4> = Point::new([f64::NAN, f64::INFINITY, f64::NAN, 1.0]);
if let Err(CoordinateValidationError::InvalidCoordinate {
coordinate_index,
dimension,
..
}) = multi_invalid.validate()
{
assert_eq!(
coordinate_index, 0,
"Should report first invalid coordinate"
);
assert_eq!(dimension, 4);
}
let invalid_1d: Point<f64, 1> = Point::new([f64::NAN]);
if let Err(CoordinateValidationError::InvalidCoordinate { dimension, .. }) =
invalid_1d.validate()
{
assert_eq!(dimension, 1);
}
let invalid_5d: Point<f64, 5> = Point::new([1.0, 2.0, f64::INFINITY, 4.0, 5.0]);
if let Err(CoordinateValidationError::InvalidCoordinate {
coordinate_index,
dimension,
..
}) = invalid_5d.validate()
{
assert_eq!(coordinate_index, 2);
assert_eq!(dimension, 5);
}
}
#[test]
fn coordinate_trait_hash_coordinate_comprehensive() {
let coord1: Point<f64, 3> = Point::new([1.0, 2.0, 3.0]);
let coord2: Point<f64, 3> = Point::new([1.0, 2.0, 3.0]);
let coord3: Point<f64, 3> = Point::new([1.0, 2.0, 4.0]);
let mut hasher1 = DefaultHasher::new();
let mut hasher2 = DefaultHasher::new();
let mut hasher3 = DefaultHasher::new();
coord1.hash_coordinate(&mut hasher1);
coord2.hash_coordinate(&mut hasher2);
coord3.hash_coordinate(&mut hasher3);
assert_eq!(
hasher1.finish(),
hasher2.finish(),
"Same coordinates should have same hash"
);
assert_ne!(
hasher1.finish(),
hasher3.finish(),
"Different coordinates should have different hash"
);
let nan_coord1: Point<f64, 2> = Point::new([f64::NAN, 1.0]);
let nan_coord2: Point<f64, 2> = Point::new([f64::NAN, 1.0]);
let mut hasher_nan1 = DefaultHasher::new();
let mut hasher_nan2 = DefaultHasher::new();
nan_coord1.hash_coordinate(&mut hasher_nan1);
nan_coord2.hash_coordinate(&mut hasher_nan2);
assert_eq!(
hasher_nan1.finish(),
hasher_nan2.finish(),
"NaN coordinates should hash consistently"
);
let inf_coord1: Point<f64, 2> = Point::new([f64::INFINITY, 1.0]);
let inf_coord2: Point<f64, 2> = Point::new([f64::INFINITY, 1.0]);
let mut hasher_inf1 = DefaultHasher::new();
let mut hasher_inf2 = DefaultHasher::new();
inf_coord1.hash_coordinate(&mut hasher_inf1);
inf_coord2.hash_coordinate(&mut hasher_inf2);
assert_eq!(
hasher_inf1.finish(),
hasher_inf2.finish(),
"Infinity coordinates should hash consistently"
);
}
#[test]
fn coordinate_trait_ordered_equals_comprehensive() {
let coord1: Point<f64, 3> = Point::new([1.0, 2.0, 3.0]);
let coord2: Point<f64, 3> = Point::new([1.0, 2.0, 3.0]);
let coord3: Point<f64, 3> = Point::new([1.0, 2.0, 4.0]);
assert!(coord1.ordered_equals(&coord2));
assert!(coord2.ordered_equals(&coord1));
assert!(!coord1.ordered_equals(&coord3));
let nan_coord1: Point<f64, 3> = Point::new([f64::NAN, 2.0, 3.0]);
let nan_coord2: Point<f64, 3> = Point::new([f64::NAN, 2.0, 3.0]);
let normal_coord: Point<f64, 3> = Point::new([1.0, 2.0, 3.0]);
assert!(nan_coord1.ordered_equals(&nan_coord2));
assert!(!nan_coord1.ordered_equals(&normal_coord));
let multi_nan1: Point<f64, 3> = Point::new([f64::NAN, f64::NAN, 3.0]);
let multi_nan2: Point<f64, 3> = Point::new([f64::NAN, f64::NAN, 3.0]);
assert!(multi_nan1.ordered_equals(&multi_nan2));
let inf_coord1: Point<f64, 2> = Point::new([f64::INFINITY, 2.0]);
let inf_coord2: Point<f64, 2> = Point::new([f64::INFINITY, 2.0]);
let neg_inf_coord: Point<f64, 2> = Point::new([f64::NEG_INFINITY, 2.0]);
assert!(inf_coord1.ordered_equals(&inf_coord2));
assert!(!inf_coord1.ordered_equals(&neg_inf_coord));
let mixed1: Point<f64, 4> = Point::new([f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1.0]);
let mixed2: Point<f64, 4> = Point::new([f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1.0]);
let mixed3: Point<f64, 4> = Point::new([f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 2.0]);
assert!(mixed1.ordered_equals(&mixed2));
assert!(!mixed1.ordered_equals(&mixed3));
}
#[test]
fn coordinate_validation_error_properties() {
let error = CoordinateValidationError::InvalidCoordinate {
coordinate_index: 1,
coordinate_value: "NaN".to_string(),
dimension: 3,
};
let debug_str = format!("{error:?}");
assert!(debug_str.contains("InvalidCoordinate"));
assert!(debug_str.contains("coordinate_index: 1"));
assert!(debug_str.contains("dimension: 3"));
let display_str = format!("{error}");
assert!(display_str.contains("Invalid coordinate at index 1 in dimension 3: NaN"));
let error_clone = error.clone();
assert_eq!(error, error_clone);
let different_error = CoordinateValidationError::InvalidCoordinate {
coordinate_index: 2,
coordinate_value: "inf".to_string(),
dimension: 3,
};
assert_ne!(error, different_error);
}
#[test]
fn coordinate_scalar_default_tolerance() {
fn test_tolerance<T: CoordinateScalar>(a: T, b: T) -> bool {
(a - b).abs() < T::default_tolerance()
}
assert_relative_eq!(
f32::default_tolerance(),
DEFAULT_TOLERANCE_F32,
epsilon = f32::EPSILON
);
assert_relative_eq!(
f64::default_tolerance(),
DEFAULT_TOLERANCE_F64,
epsilon = f64::EPSILON
);
assert_relative_eq!(f32::default_tolerance(), 1e-6_f32, epsilon = f32::EPSILON);
assert_relative_eq!(f64::default_tolerance(), 1e-15_f64, epsilon = f64::EPSILON);
let a_f32 = 1.0f32;
let b_f32 = 1.0f32 + f32::default_tolerance() / 2.0;
assert!(test_tolerance(a_f32, b_f32));
let a_f64 = 1.0f64;
let b_f64 = 1.0f64 + f64::default_tolerance() / 2.0;
assert!(test_tolerance(a_f64, b_f64));
assert!(f64::from(f32::default_tolerance()) > f64::default_tolerance());
}
#[test]
fn coordinate_trait_hash_collision_resistance() {
use std::collections::HashSet;
let mut hashes = HashSet::new();
let test_coords = [
[1.0, 2.0, 3.0],
[1.1, 2.0, 3.0],
[1.0, 2.1, 3.0],
[1.0, 2.0, 3.1],
[0.0, 0.0, 0.0],
[-1.0, -2.0, -3.0],
[1e10, 1e-10, 0.0],
];
for coords in test_coords {
let coord: Point<f64, 3> = Point::new(coords);
let mut hash_builder = DefaultHasher::new();
coord.hash_coordinate(&mut hash_builder);
hashes.insert(hash_builder.finish());
}
assert_eq!(
hashes.len(),
test_coords.len(),
"Hash collision detected in basic test set"
);
}
#[test]
fn coordinate_constants_correctness() {
const _F32_POSITIVE: () = assert!(DEFAULT_TOLERANCE_F32 > 0.0);
const _F64_POSITIVE: () = assert!(DEFAULT_TOLERANCE_F64 > 0.0);
assert!(f64::from(DEFAULT_TOLERANCE_F32) > DEFAULT_TOLERANCE_F64);
assert_relative_eq!(DEFAULT_TOLERANCE_F32, 1e-6, epsilon = f32::EPSILON);
assert_relative_eq!(DEFAULT_TOLERANCE_F64, 1e-15, epsilon = f64::EPSILON);
}
#[test]
fn coordinate_scalar_trait_bounds_comprehensive() {
fn test_bounds<T: CoordinateScalar>() {
let zero = T::zero();
let nan = T::nan();
assert!(zero.ordered_eq(&T::zero()));
assert!(nan.ordered_eq(&T::nan())); assert!(zero.is_finite_generic());
assert!(!nan.is_finite_generic());
assert_eq!(T::default(), T::zero());
assert!(T::default_tolerance() > T::zero());
assert!(T::mantissa_digits() > 0);
}
test_bounds::<f32>();
test_bounds::<f64>();
assert_eq!(f32::mantissa_digits(), 24);
assert_eq!(f64::mantissa_digits(), 53);
assert_relative_eq!(f32::default_tolerance(), 1e-6_f32, epsilon = f32::EPSILON);
assert_relative_eq!(f64::default_tolerance(), 1e-15_f64, epsilon = f64::EPSILON);
}
#[test]
fn coordinate_validation_error_source_trait() {
use std::error::Error;
let error = CoordinateValidationError::InvalidCoordinate {
coordinate_index: 1,
coordinate_value: "NaN".to_string(),
dimension: 3,
};
assert!(error.source().is_none());
let _boxed_error: Box<dyn Error> = Box::new(error.clone());
let error_ref: &dyn Error = &error;
assert_eq!(error_ref.to_string(), error.to_string());
}
#[test]
fn coordinate_trait_dimension_consistency() {
const DIM_1D: usize = 1;
const DIM_7D: usize = 7;
let coord_1d: Point<f64, 1> = Point::new([42.0]);
assert_eq!(coord_1d.dim(), 1);
assert_eq!(coord_1d.to_array().len(), 1);
let coord_7d: Point<f64, 7> = Point::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
assert_eq!(coord_7d.dim(), 7);
assert_eq!(coord_7d.to_array().len(), 7);
assert_eq!(coord_1d.dim(), DIM_1D);
assert_eq!(coord_7d.dim(), DIM_7D);
}
}