#![forbid(unsafe_code)]
use crate::geometry::matrix::{matrix_get, matrix_set, matrix_zero_like};
use crate::geometry::point::Point;
use crate::geometry::predicates::{InSphere, Orientation, insphere_lifted, simplex_orientation};
use crate::geometry::robust_predicates::{robust_insphere, robust_orientation};
use crate::geometry::sos::exact_det_sign;
use crate::geometry::traits::coordinate::{
Coordinate, CoordinateConversionError, CoordinateScalar,
};
use crate::geometry::util::{safe_coords_to_f64, safe_scalar_to_f64, squared_norm};
use core::marker::PhantomData;
pub trait Kernel<const D: usize>: Clone + Default {
type Scalar: CoordinateScalar;
fn orientation(
&self,
points: &[Point<Self::Scalar, D>],
) -> Result<i32, CoordinateConversionError>;
fn in_sphere(
&self,
simplex_points: &[Point<Self::Scalar, D>],
test_point: &Point<Self::Scalar, D>,
) -> Result<i32, CoordinateConversionError>;
}
pub trait ExactPredicates {}
impl<T: CoordinateScalar> ExactPredicates for RobustKernel<T> {}
impl<T: CoordinateScalar> ExactPredicates for AdaptiveKernel<T> {}
#[derive(Clone, Default, Debug)]
pub struct FastKernel<T: CoordinateScalar> {
_phantom: PhantomData<T>,
}
impl<T: CoordinateScalar> FastKernel<T> {
#[must_use]
pub const fn new() -> Self {
Self {
_phantom: PhantomData,
}
}
}
impl<T, const D: usize> Kernel<D> for FastKernel<T>
where
T: CoordinateScalar,
{
type Scalar = T;
fn orientation(
&self,
points: &[Point<Self::Scalar, D>],
) -> Result<i32, CoordinateConversionError> {
let result = simplex_orientation(points)?;
Ok(match result {
Orientation::NEGATIVE => -1,
Orientation::DEGENERATE => 0,
Orientation::POSITIVE => 1,
})
}
fn in_sphere(
&self,
simplex_points: &[Point<Self::Scalar, D>],
test_point: &Point<Self::Scalar, D>,
) -> Result<i32, CoordinateConversionError> {
let result = insphere_lifted(simplex_points, *test_point).map_err(|e| {
match e {
crate::core::cell::CellValidationError::CoordinateConversion { source } => source,
_ => CoordinateConversionError::ConversionFailed {
coordinate_index: 0,
coordinate_value: format!("{e}"),
from_type: "insphere_lifted",
to_type: "in_sphere",
},
}
})?;
Ok(match result {
InSphere::OUTSIDE => -1,
InSphere::BOUNDARY => 0,
InSphere::INSIDE => 1,
})
}
}
#[derive(Clone, Default, Debug)]
pub struct RobustKernel<T: CoordinateScalar> {
_phantom: PhantomData<T>,
}
impl<T: CoordinateScalar> RobustKernel<T> {
#[must_use]
pub const fn new() -> Self {
Self {
_phantom: PhantomData,
}
}
}
impl<T, const D: usize> Kernel<D> for RobustKernel<T>
where
T: CoordinateScalar,
{
type Scalar = T;
fn orientation(
&self,
points: &[Point<Self::Scalar, D>],
) -> Result<i32, CoordinateConversionError> {
let result = robust_orientation(points)?;
Ok(match result {
Orientation::NEGATIVE => -1,
Orientation::DEGENERATE => 0,
Orientation::POSITIVE => 1,
})
}
fn in_sphere(
&self,
simplex_points: &[Point<Self::Scalar, D>],
test_point: &Point<Self::Scalar, D>,
) -> Result<i32, CoordinateConversionError> {
let result = robust_insphere(simplex_points, test_point)?;
Ok(match result {
InSphere::OUTSIDE => -1,
InSphere::BOUNDARY => 0,
InSphere::INSIDE => 1,
})
}
}
#[derive(Clone, Default, Debug)]
pub struct AdaptiveKernel<T: CoordinateScalar> {
_phantom: PhantomData<T>,
}
impl<T: CoordinateScalar> AdaptiveKernel<T> {
#[must_use]
pub const fn new() -> Self {
Self {
_phantom: PhantomData,
}
}
}
impl<T, const D: usize> Kernel<D> for AdaptiveKernel<T>
where
T: CoordinateScalar,
{
type Scalar = T;
fn orientation(
&self,
points: &[Point<Self::Scalar, D>],
) -> Result<i32, CoordinateConversionError> {
if points.len() != D + 1 {
return Err(CoordinateConversionError::ConversionFailed {
coordinate_index: 0,
coordinate_value: format!("Expected {} points, got {}", D + 1, points.len()),
from_type: "point count",
to_type: "valid simplex",
});
}
let exact = robust_orientation(points)?;
match exact {
Orientation::POSITIVE => return Ok(1),
Orientation::NEGATIVE => return Ok(-1),
Orientation::DEGENERATE => {}
}
let f64_points: Vec<Point<f64, D>> = points
.iter()
.map(|p| safe_coords_to_f64(p.coords()).map(Point::new))
.collect::<Result<_, _>>()?;
crate::geometry::sos::sos_orientation_sign(&f64_points).map_or(Ok(0), Ok)
}
fn in_sphere(
&self,
simplex_points: &[Point<Self::Scalar, D>],
test_point: &Point<Self::Scalar, D>,
) -> Result<i32, CoordinateConversionError> {
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| {
let ref_coords = simplex_points[0].coords();
for (row, point) in simplex_points.iter().skip(1).enumerate() {
let coords = point.coords();
let mut rel_t: [T; D] = [T::zero(); D];
for (dst, (p, r)) in rel_t.iter_mut().zip(coords.iter().zip(ref_coords.iter())) {
*dst = *p - *r;
}
let rel_f64: [f64; D] = safe_coords_to_f64(&rel_t)?;
for (j, &v) in rel_f64.iter().enumerate() {
matrix_set(&mut matrix, row, j, v);
}
let sq_norm = squared_norm(&rel_t);
let sq_f64 = safe_scalar_to_f64(sq_norm)?;
matrix_set(&mut matrix, row, D, sq_f64);
}
let test_coords = test_point.coords();
let mut test_rel_t: [T; D] = [T::zero(); D];
for (dst, (p, r)) in test_rel_t
.iter_mut()
.zip(test_coords.iter().zip(ref_coords.iter()))
{
*dst = *p - *r;
}
let test_rel_f64: [f64; D] = safe_coords_to_f64(&test_rel_t)?;
for (j, &v) in test_rel_f64.iter().enumerate() {
matrix_set(&mut matrix, D, j, v);
}
let test_sq = squared_norm(&test_rel_t);
let test_sq_f64 = safe_scalar_to_f64(test_sq)?;
matrix_set(&mut matrix, D, D, test_sq_f64);
let mut orient_matrix = matrix_zero_like(&matrix);
for i in 0..D {
for j in 0..D {
matrix_set(&mut orient_matrix, i, j, matrix_get(&matrix, i, j));
}
}
matrix_set(&mut orient_matrix, D, D, 1.0);
let rel_orient_sign = exact_det_sign(&orient_matrix);
let insphere_det_sign = exact_det_sign(&matrix);
if rel_orient_sign != 0 && insphere_det_sign != 0 {
let orient_factor = -rel_orient_sign;
return Ok((insphere_det_sign * orient_factor).signum());
}
let f64_simplex: Vec<Point<f64, D>> = simplex_points
.iter()
.map(|p| safe_coords_to_f64(p.coords()).map(Point::new))
.collect::<Result<_, _>>()?;
let f64_test = Point::new(safe_coords_to_f64(test_point.coords())?);
let orient_factor: i32 = if rel_orient_sign != 0 {
-rel_orient_sign
} else {
let sos_abs = crate::geometry::sos::sos_orientation_sign(&f64_simplex)?;
if D.is_multiple_of(2) {
-sos_abs
} else {
sos_abs
}
};
let insphere_effective: i32 = if insphere_det_sign != 0 {
insphere_det_sign
} else {
crate::geometry::sos::sos_insphere_sign(&f64_simplex, &f64_test)?
};
Ok((insphere_effective * orient_factor).signum())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::point::Point;
use crate::geometry::traits::coordinate::Coordinate;
fn standard_simplex<const D: usize>() -> Vec<Point<f64, D>> {
let mut points = Vec::with_capacity(D + 1);
points.push(Point::new([0.0; D]));
for i in 0..D {
let mut coords = [0.0; D];
coords[i] = 1.0;
points.push(Point::new(coords));
}
points
}
fn degenerate_simplex<const D: usize>() -> Vec<Point<f64, D>> {
let mut points = Vec::with_capacity(D + 1);
points.push(Point::new([0.0; D]));
for i in 0..D.saturating_sub(1) {
let mut coords = [0.0; D];
coords[i] = 1.0;
points.push(Point::new(coords));
}
let mut bary = [0.0; D];
for c in bary.iter_mut().take(D.saturating_sub(1)) {
*c = 0.5;
}
points.push(Point::new(bary));
points
}
fn inside_point<const D: usize>() -> Point<f64, D> {
Point::new([0.1; D])
}
fn outside_point<const D: usize>() -> Point<f64, D> {
Point::new([2.0; D])
}
fn cospherical_test<const D: usize>() -> Point<f64, D> {
Point::new([1.0; D])
}
fn off_plane_test<const D: usize>() -> Point<f64, D> {
let mut coords = [0.0; D];
coords[D - 1] = 1.0;
Point::new(coords)
}
fn coplanar_far_test<const D: usize>() -> Point<f64, D> {
let mut coords = [0.0; D];
coords[0] = 3.0;
Point::new(coords)
}
macro_rules! gen_standard_kernel_tests {
($dim:literal, $name:ident, $kernel:expr) => {
pastey::paste! {
#[test]
fn [<test_ $name _orientation_ $dim d_nondegenerate>]() {
let kernel = $kernel;
let simplex = standard_simplex::<$dim>();
let result = kernel.orientation(&simplex).unwrap();
assert!(
result == 1 || result == -1,
"expected ±1, got {result}"
);
}
#[test]
fn [<test_ $name _orientation_ $dim d_degenerate>]() {
let kernel = $kernel;
let simplex = degenerate_simplex::<$dim>();
let result = kernel.orientation(&simplex).unwrap();
assert_eq!(result, 0, "degenerate simplex must give 0");
}
#[test]
fn [<test_ $name _insphere_ $dim d_inside>]() {
let kernel = $kernel;
let simplex = standard_simplex::<$dim>();
let test = inside_point::<$dim>();
let result = kernel.in_sphere(&simplex, &test).unwrap();
assert_eq!(result, 1, "point should be INSIDE");
}
#[test]
fn [<test_ $name _insphere_ $dim d_outside>]() {
let kernel = $kernel;
let simplex = standard_simplex::<$dim>();
let test = outside_point::<$dim>();
let result = kernel.in_sphere(&simplex, &test).unwrap();
assert_eq!(result, -1, "point should be OUTSIDE");
}
}
};
}
gen_standard_kernel_tests!(2, fast, FastKernel::<f64>::new());
gen_standard_kernel_tests!(3, fast, FastKernel::<f64>::new());
gen_standard_kernel_tests!(4, fast, FastKernel::<f64>::new());
gen_standard_kernel_tests!(5, fast, FastKernel::<f64>::new());
gen_standard_kernel_tests!(2, robust, RobustKernel::<f64>::new());
gen_standard_kernel_tests!(3, robust, RobustKernel::<f64>::new());
gen_standard_kernel_tests!(4, robust, RobustKernel::<f64>::new());
gen_standard_kernel_tests!(5, robust, RobustKernel::<f64>::new());
#[test]
fn test_orientation_collinear_diagonal_2d() {
let kernel = FastKernel::<f64>::new();
let collinear = [
Point::new([0.0, 0.0]),
Point::new([1.0, 1.0]),
Point::new([2.0, 2.0]),
];
let orientation = kernel.orientation(&collinear).unwrap();
assert_eq!(
orientation, 0,
"Diagonal collinear points should have zero orientation"
);
}
#[test]
fn test_orientation_nearly_collinear_2d_robust() {
let kernel = RobustKernel::<f64>::new();
let nearly_collinear = [
Point::new([0.0, 0.0]),
Point::new([1.0, 0.0]),
Point::new([2.0, 1e-10]), ];
let orientation = kernel.orientation(&nearly_collinear).unwrap();
assert!(orientation == -1 || orientation == 0 || orientation == 1);
}
#[test]
fn test_orientation_extreme_coordinates_2d() {
let kernel = RobustKernel::<f64>::new();
let large_triangle = [
Point::new([1e6, 1e6]),
Point::new([1e6 + 1.0, 1e6]),
Point::new([1e6, 1e6 + 1.0]),
];
let orientation = kernel.orientation(&large_triangle).unwrap();
assert_ne!(
orientation, 0,
"Triangle with large coordinates should be non-degenerate"
);
}
#[test]
fn test_orientation_small_but_valid_2d() {
let kernel = FastKernel::<f64>::new();
let small_triangle = [
Point::new([0.0, 0.0]),
Point::new([1e-6, 0.0]),
Point::new([0.0, 1e-6]),
];
let orientation = kernel.orientation(&small_triangle).unwrap();
assert_ne!(
orientation, 0,
"Small but valid triangle should be non-degenerate"
);
}
#[test]
fn test_kernel_default_trait() {
let _fast: FastKernel<f64> = FastKernel::default();
let _robust: RobustKernel<f64> = RobustKernel::default();
}
#[test]
fn test_fast_kernel_in_sphere_insufficient_vertices() {
let kernel = FastKernel::<f64>::new();
let simplex: [Point<f64, 3>; 2] =
[Point::new([0.0, 0.0, 0.0]), Point::new([1.0, 0.0, 0.0])];
let test_point = Point::new([0.5, 0.5, 0.5]);
let result = kernel.in_sphere(&simplex, &test_point);
assert!(result.is_err(), "Should error with insufficient vertices");
}
#[test]
fn test_fast_kernel_in_sphere_degenerate_simplex() {
let kernel = FastKernel::<f64>::new();
let simplex = [
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([1.0, 1.0, 0.0]), ];
let test_point = Point::new([0.5, 0.5, 0.5]);
let result = kernel.in_sphere(&simplex, &test_point);
assert!(result.is_err(), "Should error with degenerate simplex");
}
macro_rules! gen_adaptive_kernel_tests {
($dim:literal) => {
pastey::paste! {
#[test]
fn [<test_adaptive_orientation_ $dim d_agrees>]() {
let adaptive = AdaptiveKernel::<f64>::new();
let fast = FastKernel::<f64>::new();
let simplex = standard_simplex::<$dim>();
assert_eq!(
adaptive.orientation(&simplex).unwrap(),
fast.orientation(&simplex).unwrap(),
"AdaptiveKernel must agree with FastKernel on non-degenerate"
);
}
#[test]
fn [<test_adaptive_orientation_ $dim d_degenerate_sos_nonzero>]() {
let kernel = AdaptiveKernel::<f64>::new();
let simplex = degenerate_simplex::<$dim>();
let result = kernel.orientation(&simplex).unwrap();
assert!(
result == 1 || result == -1,
"AdaptiveKernel SoS orientation must never return 0, got {result}"
);
}
#[test]
fn [<test_adaptive_orientation_ $dim d_degenerate_deterministic>]() {
let kernel = AdaptiveKernel::<f64>::new();
let simplex = degenerate_simplex::<$dim>();
let results: Vec<i32> = (0..10)
.map(|_| kernel.orientation(&simplex).unwrap())
.collect();
assert!(
results.iter().all(|&r| r == results[0]),
"Degenerate SoS orientation must be deterministic"
);
}
#[test]
fn [<test_adaptive_insphere_ $dim d_agrees>]() {
let adaptive = AdaptiveKernel::<f64>::new();
let fast = FastKernel::<f64>::new();
let simplex = standard_simplex::<$dim>();
let inside = inside_point::<$dim>();
assert_eq!(
adaptive.in_sphere(&simplex, &inside).unwrap(),
fast.in_sphere(&simplex, &inside).unwrap(),
"Must agree on clearly inside point"
);
let outside = outside_point::<$dim>();
assert_eq!(
adaptive.in_sphere(&simplex, &outside).unwrap(),
fast.in_sphere(&simplex, &outside).unwrap(),
"Must agree on clearly outside point"
);
}
#[test]
fn [<test_adaptive_insphere_ $dim d_cospherical_nonzero>]() {
let kernel = AdaptiveKernel::<f64>::new();
let simplex = standard_simplex::<$dim>();
let test = cospherical_test::<$dim>();
let result = kernel.in_sphere(&simplex, &test).unwrap();
assert!(
result == 1 || result == -1,
"AdaptiveKernel insphere must never return 0, got {result}"
);
}
#[test]
fn [<test_adaptive_insphere_ $dim d_cospherical_deterministic>]() {
let kernel = AdaptiveKernel::<f64>::new();
let simplex = standard_simplex::<$dim>();
let test = cospherical_test::<$dim>();
let results: Vec<i32> = (0..10)
.map(|_| kernel.in_sphere(&simplex, &test).unwrap())
.collect();
assert!(
results.iter().all(|&r| r == results[0]),
"Degenerate insphere must be deterministic"
);
}
#[test]
fn [<test_adaptive_insphere_ $dim d_orient_degenerate>]() {
let kernel = AdaptiveKernel::<f64>::new();
let simplex = degenerate_simplex::<$dim>();
let test = off_plane_test::<$dim>();
let result = kernel.in_sphere(&simplex, &test).unwrap();
assert!(
result == 1 || result == -1,
"Must resolve orient-degenerate insphere, got {result}"
);
}
#[test]
fn [<test_adaptive_insphere_ $dim d_both_degenerate>]() {
let kernel = AdaptiveKernel::<f64>::new();
let simplex = degenerate_simplex::<$dim>();
let test = coplanar_far_test::<$dim>();
let result = kernel.in_sphere(&simplex, &test).unwrap();
assert!(
result == 1 || result == -1,
"Must resolve both-degenerate insphere, got {result}"
);
}
}
};
}
gen_adaptive_kernel_tests!(2);
gen_adaptive_kernel_tests!(3);
gen_adaptive_kernel_tests!(4);
gen_adaptive_kernel_tests!(5);
macro_rules! gen_kernel_consistency_tests {
($dim:literal) => {
pastey::paste! {
#[test]
fn [<test_kernel_consistency_ $dim d_orientation>]() {
let fast = FastKernel::<f64>::new();
let robust = RobustKernel::<f64>::new();
let adaptive = AdaptiveKernel::<f64>::new();
let simplex = standard_simplex::<$dim>();
let f = fast.orientation(&simplex).unwrap();
let r = robust.orientation(&simplex).unwrap();
let a = adaptive.orientation(&simplex).unwrap();
assert_eq!(f, r, "Fast vs Robust");
assert_eq!(f, a, "Fast vs Adaptive");
}
#[test]
fn [<test_kernel_consistency_ $dim d_insphere>]() {
let fast = FastKernel::<f64>::new();
let robust = RobustKernel::<f64>::new();
let adaptive = AdaptiveKernel::<f64>::new();
let simplex = standard_simplex::<$dim>();
let test = inside_point::<$dim>();
let f = fast.in_sphere(&simplex, &test).unwrap();
let r = robust.in_sphere(&simplex, &test).unwrap();
let a = adaptive.in_sphere(&simplex, &test).unwrap();
assert_eq!(f, r, "Fast vs Robust");
assert_eq!(f, a, "Fast vs Adaptive");
}
}
};
}
gen_kernel_consistency_tests!(2);
gen_kernel_consistency_tests!(3);
gen_kernel_consistency_tests!(4);
gen_kernel_consistency_tests!(5);
#[test]
fn test_adaptive_kernel_default_trait() {
let _adaptive: AdaptiveKernel<f64> = AdaptiveKernel::default();
}
#[test]
fn test_adaptive_kernel_wrong_point_count() {
let kernel = AdaptiveKernel::<f64>::new();
let points = [Point::new([0.0, 0.0]), Point::new([1.0, 0.0])];
assert!(kernel.orientation(&points).is_err());
let simplex = [Point::new([0.0, 0.0]), Point::new([1.0, 0.0])];
let test = Point::new([0.5, 0.5]);
assert!(kernel.in_sphere(&simplex, &test).is_err());
}
macro_rules! gen_sos_identical_points_test {
($dim:literal) => {
pastey::paste! {
#[test]
fn [<test_adaptive_sos_identical_points_ $dim d>]() {
let kernel = AdaptiveKernel::<f64>::new();
let points: Vec<Point<f64, $dim>> =
vec![Point::new([0.42; $dim]); $dim + 1];
let result = kernel.orientation(&points).unwrap();
assert_eq!(
result, 0,
"{}D: identical points must yield orientation 0, got {result}",
$dim
);
}
}
};
}
gen_sos_identical_points_test!(2);
gen_sos_identical_points_test!(3);
gen_sos_identical_points_test!(4);
gen_sos_identical_points_test!(5);
const fn assert_exact_predicates<T: ExactPredicates>() {}
#[test]
fn test_adaptive_kernel_implements_exact_predicates() {
assert_exact_predicates::<AdaptiveKernel<f64>>();
assert_exact_predicates::<AdaptiveKernel<f32>>();
}
#[test]
fn test_robust_kernel_implements_exact_predicates() {
assert_exact_predicates::<RobustKernel<f64>>();
assert_exact_predicates::<RobustKernel<f32>>();
}
#[test]
fn test_fast_kernel_does_not_implement_exact_predicates() {
fn _requires_exact<T: ExactPredicates>() {}
}
}