#![forbid(unsafe_code)]
use thiserror::Error;
use crate::core::collections::{MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer};
use crate::geometry::periodic::ToroidalDomain;
use crate::geometry::point::{Point, ValidatedCoordinates};
use crate::geometry::predicates::Orientation;
use crate::geometry::robust_predicates::robust_orientation;
use crate::geometry::traits::coordinate::InvalidCoordinateValue;
use crate::geometry::util::simplex_lp::{
IntersectionLinearProgramResult, coordinates_are_identical,
intersection_via_legacy_active_sets, intersection_via_linear_program,
shared_face_fast_confinement,
};
pub type SimplexRealizationBuffer<T> = SmallBuffer<T, MAX_PRACTICAL_DIMENSION_SIZE>;
#[derive(Clone, Debug, PartialEq)]
pub struct LabeledSimplexRealization<L, const D: usize> {
labels: SimplexRealizationBuffer<L>,
coordinates: SimplexRealizationBuffer<[f64; D]>,
}
impl<L, const D: usize> LabeledSimplexRealization<L, D> {
pub fn try_new(
labels: impl IntoIterator<Item = L>,
coordinates: impl IntoIterator<Item = [f64; D]>,
) -> Result<Self, LabeledSimplexRealizationError>
where
L: Eq,
{
let labels: SimplexRealizationBuffer<L> = labels.into_iter().collect();
let coordinates: SimplexRealizationBuffer<[f64; D]> = coordinates.into_iter().collect();
if labels.len() != coordinates.len() {
return Err(
LabeledSimplexRealizationError::LabelCoordinateLengthMismatch {
label_count: labels.len(),
coordinate_count: coordinates.len(),
},
);
}
let expected = D + 1;
if labels.len() != expected {
return Err(LabeledSimplexRealizationError::InvalidArity {
expected,
actual: labels.len(),
});
}
for (first_index, first_label) in labels.iter().enumerate() {
if let Some(duplicate_offset) = labels[first_index + 1..]
.iter()
.position(|label| label == first_label)
{
return Err(LabeledSimplexRealizationError::DuplicateLabel {
first_index,
duplicate_index: first_index + duplicate_offset + 1,
});
}
}
validate_coordinate_rows(&coordinates)?;
Ok(Self {
labels,
coordinates,
})
}
pub fn labels(&self) -> &[L] {
&self.labels
}
pub fn coordinates(&self) -> &[[f64; D]] {
&self.coordinates
}
pub(crate) fn point_at(&self, vertex_index: usize) -> Option<Point<D>> {
self.coordinates.get(vertex_index).copied().map(|coords| {
Point::from_validated_coordinates(
ValidatedCoordinates::from_prevalidated_finite_values(coords),
)
})
}
pub fn try_translated(
&self,
domain: &ToroidalDomain<D>,
shift: &[i32; D],
) -> Result<Self, LabeledSimplexRealizationError>
where
L: Clone,
{
let periods = domain.periods();
let mut translated_coordinates = self.coordinates.clone();
for coords in &mut translated_coordinates {
for axis in 0..D {
coords[axis] = f64::from(shift[axis]).mul_add(periods[axis], coords[axis]);
}
}
validate_coordinate_rows(&translated_coordinates)?;
Ok(Self {
labels: self.labels.clone(),
coordinates: translated_coordinates,
})
}
}
fn validate_coordinate_rows<const D: usize>(
coordinates: &SimplexRealizationBuffer<[f64; D]>,
) -> Result<(), LabeledSimplexRealizationError> {
for (vertex_index, coords) in coordinates.iter().enumerate() {
for (coordinate_index, coordinate) in coords.iter().enumerate() {
if !coordinate.is_finite() {
return Err(LabeledSimplexRealizationError::NonFiniteCoordinate {
vertex_index,
coordinate_index,
coordinate_value: InvalidCoordinateValue::from_debug(coordinate),
});
}
}
}
Ok(())
}
#[derive(Clone, Debug, Error, PartialEq)]
#[non_exhaustive]
pub enum LabeledSimplexRealizationError {
#[error("label count {label_count} does not match coordinate count {coordinate_count}")]
LabelCoordinateLengthMismatch {
label_count: usize,
coordinate_count: usize,
},
#[error("invalid simplex realization arity: expected {expected}, got {actual}")]
InvalidArity {
expected: usize,
actual: usize,
},
#[error("duplicate simplex realization label at indices {first_index} and {duplicate_index}")]
DuplicateLabel {
first_index: usize,
duplicate_index: usize,
},
#[error(
"non-finite coordinate at vertex {vertex_index}, coordinate {coordinate_index}: {coordinate_value}"
)]
NonFiniteCoordinate {
vertex_index: usize,
coordinate_index: usize,
coordinate_value: InvalidCoordinateValue,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SimplexIntersectionWitness<L> {
pub shared: SimplexRealizationBuffer<L>,
pub first_only_witness: SimplexRealizationBuffer<L>,
pub second_only_witness: SimplexRealizationBuffer<L>,
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
#[non_exhaustive]
pub enum SimplexIntersectionFailure<L> {
#[error(
"exact simplex-intersection validation does not support dimension {dimension} (maximum {max})"
)]
UnsupportedDimension {
dimension: usize,
max: usize,
},
#[error("simplex barycentric basis is singular")]
SingularBarycentricBasis,
#[error("simplices intersect outside their shared face")]
#[non_exhaustive]
IntersectionOutsideSharedFace {
witness: SimplexIntersectionWitness<L>,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PeriodicSimplexSpan {
axis: usize,
span: f64,
period: f64,
}
impl PeriodicSimplexSpan {
#[must_use]
pub const fn axis(&self) -> usize {
self.axis
}
#[must_use]
pub const fn span(&self) -> f64 {
self.span
}
#[must_use]
pub const fn period(&self) -> f64 {
self.period
}
}
pub fn coordinate_range_for_axis<L, const D: usize>(
simplex: &LabeledSimplexRealization<L, D>,
axis: usize,
) -> Option<(f64, f64)> {
if axis >= D {
return None;
}
Some(simplex.coordinates.iter().fold(
(f64::INFINITY, f64::NEG_INFINITY),
|(min_coord, max_coord), coords| (min_coord.min(coords[axis]), max_coord.max(coords[axis])),
))
}
pub fn axis_aligned_bounding_boxes_overlap<L1, L2, const D: usize>(
first: &LabeledSimplexRealization<L1, D>,
second: &LabeledSimplexRealization<L2, D>,
) -> bool {
(0..D).all(|axis| {
let Some((first_min, first_max)) = coordinate_range_for_axis(first, axis) else {
return false;
};
let Some((second_min, second_max)) = coordinate_range_for_axis(second, axis) else {
return false;
};
first_max >= second_min && second_max >= first_min
})
}
#[must_use]
pub fn periodic_simplex_span<L, const D: usize>(
simplex: &LabeledSimplexRealization<L, D>,
domain: &ToroidalDomain<D>,
) -> Option<PeriodicSimplexSpan> {
for (axis, &period) in domain.periods().iter().enumerate() {
let (min_coord, max_coord) = simplex.coordinates().iter().fold(
(f64::INFINITY, f64::NEG_INFINITY),
|(min_coord, max_coord), coords| {
let coord = coords[axis];
(min_coord.min(coord), max_coord.max(coord))
},
);
let span = max_coord - min_coord;
if span >= period {
return Some(PeriodicSimplexSpan { axis, span, period });
}
}
None
}
pub fn validate_simplex_intersection<L, const D: usize>(
first: &LabeledSimplexRealization<L, D>,
second: &LabeledSimplexRealization<L, D>,
) -> Result<(), SimplexIntersectionFailure<L>>
where
L: Clone + Eq,
{
if D > 7 {
return Err(SimplexIntersectionFailure::UnsupportedDimension {
dimension: D,
max: 7,
});
}
let shared_labels = shared_labels(first, second);
let basis_orientation = realization_orientation(first);
if basis_orientation == Some(Orientation::DEGENERATE) {
return Err(SimplexIntersectionFailure::SingularBarycentricBasis);
}
let second_orientation = realization_orientation(second);
if second_orientation == Some(Orientation::DEGENERATE) {
return intersection_result(intersection_via_legacy_active_sets(
first,
second,
&shared_labels,
));
}
if shared_face_fast_confinement(first, second, &shared_labels) {
return Ok(());
}
if let Some(orientation) = basis_orientation
&& (simplex_is_strictly_outside_a_facet(first, second, orientation)
|| second_orientation.is_some_and(|orientation| {
simplex_is_strictly_outside_a_facet(second, first, orientation)
})
|| intersection_is_confined_by_orientation(first, second, &shared_labels, orientation))
{
return Ok(());
}
intersection_result(intersection_via_linear_program(
first,
second,
&shared_labels,
basis_orientation.is_none(),
))
}
fn intersection_result<L>(
result: IntersectionLinearProgramResult<L>,
) -> Result<(), SimplexIntersectionFailure<L>> {
match result {
IntersectionLinearProgramResult::Valid => Ok(()),
IntersectionLinearProgramResult::Invalid(witness) => {
Err(SimplexIntersectionFailure::IntersectionOutsideSharedFace { witness })
}
IntersectionLinearProgramResult::SingularBarycentricBasis => {
Err(SimplexIntersectionFailure::SingularBarycentricBasis)
}
}
}
fn realization_orientation<L, const D: usize>(
simplex: &LabeledSimplexRealization<L, D>,
) -> Option<Orientation> {
let points: SimplexRealizationBuffer<_> = (0..simplex.coordinates().len())
.filter_map(|index| simplex.point_at(index))
.collect();
robust_orientation(&points).ok()
}
fn simplex_is_strictly_outside_a_facet<L, const D: usize>(
basis: &LabeledSimplexRealization<L, D>,
other: &LabeledSimplexRealization<L, D>,
basis_orientation: Orientation,
) -> bool {
let basis_points: SimplexRealizationBuffer<_> = (0..basis.coordinates().len())
.filter_map(|index| basis.point_at(index))
.collect();
(0..basis.labels().len()).any(|basis_index| {
(0..other.coordinates().len()).all(|other_index| {
let Some(other_point) = other.point_at(other_index) else {
return false;
};
let mut replaced_points = basis_points.clone();
replaced_points[basis_index] = other_point;
robust_orientation(&replaced_points).is_ok_and(|orientation| {
orientation != basis_orientation && orientation != Orientation::DEGENERATE
})
})
})
}
fn intersection_is_confined_by_orientation<L, const D: usize>(
basis: &LabeledSimplexRealization<L, D>,
other: &LabeledSimplexRealization<L, D>,
shared_labels: &[L],
basis_orientation: Orientation,
) -> bool
where
L: Eq,
{
for shared_label in shared_labels {
let Some(basis_index) = basis
.labels()
.iter()
.position(|candidate| candidate == shared_label)
else {
return false;
};
let Some(other_index) = other
.labels()
.iter()
.position(|candidate| candidate == shared_label)
else {
return false;
};
if !coordinates_are_identical(
&basis.coordinates()[basis_index],
&other.coordinates()[other_index],
) {
return false;
}
}
let basis_points: SimplexRealizationBuffer<_> = (0..basis.coordinates().len())
.filter_map(|index| basis.point_at(index))
.collect();
for (basis_index, basis_label) in basis.labels().iter().enumerate() {
if shared_labels.contains(basis_label) {
continue;
}
for other_index in 0..other.coordinates().len() {
let Some(other_point) = other.point_at(other_index) else {
return false;
};
let mut replaced_points = basis_points.clone();
replaced_points[basis_index] = other_point;
let Ok(replaced_orientation) = robust_orientation(&replaced_points) else {
return false;
};
if replaced_orientation == basis_orientation {
return false;
}
}
}
true
}
fn shared_labels<L, const D: usize>(
first: &LabeledSimplexRealization<L, D>,
second: &LabeledSimplexRealization<L, D>,
) -> SimplexRealizationBuffer<L>
where
L: Clone + Eq,
{
first
.labels()
.iter()
.filter(|label| second.labels().contains(label))
.cloned()
.collect()
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use approx::assert_abs_diff_eq;
use super::*;
#[derive(Clone)]
struct CloneOnlyLabel;
#[test]
fn labeled_simplex_realization_rejects_label_coordinate_length_mismatch() {
let err =
LabeledSimplexRealization::<_, 2>::try_new(vec![0, 1, 2], vec![[0.0, 0.0], [1.0, 0.0]])
.unwrap_err();
assert_matches!(
err,
LabeledSimplexRealizationError::LabelCoordinateLengthMismatch {
label_count: 3,
coordinate_count: 2,
}
);
}
#[test]
fn labeled_simplex_realization_rejects_invalid_arity() {
let err = LabeledSimplexRealization::<_, 2>::try_new(
vec![0, 1, 2, 3],
vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
)
.unwrap_err();
assert_matches!(
err,
LabeledSimplexRealizationError::InvalidArity {
expected: 3,
actual: 4,
}
);
}
#[test]
fn labeled_simplex_realization_rejects_duplicate_labels() {
let err = LabeledSimplexRealization::<_, 2>::try_new(
vec![0, 1, 0],
vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
)
.unwrap_err();
assert_matches!(
err,
LabeledSimplexRealizationError::DuplicateLabel {
first_index: 0,
duplicate_index: 2,
}
);
}
#[test]
fn coordinate_range_rejects_out_of_bounds_axis() {
let simplex = LabeledSimplexRealization::try_new(
vec![0, 1, 2],
vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
)
.unwrap();
assert_eq!(coordinate_range_for_axis(&simplex, 2), None);
}
#[test]
fn disjoint_triangles_do_not_intersect_outside_shared_face() {
let first = LabeledSimplexRealization::try_new(
vec![0, 1, 2],
vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
)
.unwrap();
let second = LabeledSimplexRealization::try_new(
vec![3, 4, 5],
vec![[2.0, 2.0], [3.0, 2.0], [2.0, 3.0]],
)
.unwrap();
assert!(validate_simplex_intersection(&first, &second).is_ok());
}
#[test]
fn orientation_confinement_rejects_mismatched_shared_coordinates() {
let first =
LabeledSimplexRealization::try_new([0, 1, 2], [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]])
.unwrap();
let second = LabeledSimplexRealization::try_new(
[0, 3, 4],
[[-1.0, -1.0], [-2.0, -1.0], [-1.0, -2.0]],
)
.unwrap();
let orientation = realization_orientation(&first).expect("standard triangle is oriented");
assert!(!intersection_is_confined_by_orientation(
&first,
&second,
&[0],
orientation,
));
}
#[test]
fn labeled_simplex_realization_rejects_non_finite_coordinates() {
let err = LabeledSimplexRealization::try_new(
vec![0, 1, 2],
vec![[0.0, 0.0], [1.0, f64::NAN], [0.0, 1.0]],
)
.unwrap_err();
assert_matches!(
err,
LabeledSimplexRealizationError::NonFiniteCoordinate {
vertex_index: 1,
coordinate_index: 1,
coordinate_value: InvalidCoordinateValue::Nan,
}
);
}
#[test]
fn labeled_simplex_realization_rehydrates_points_from_validated_rows() {
let simplex = LabeledSimplexRealization::try_new(
vec![0, 1, 2],
vec![[-0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
)
.unwrap();
let point = simplex.point_at(0).expect("vertex index exists");
assert_eq!(point.coords()[0].to_bits(), 0.0_f64.to_bits());
assert_eq!(point.coords()[1].to_bits(), 0.0_f64.to_bits());
assert!(simplex.point_at(3).is_none());
}
#[test]
fn translated_realization_rejects_non_finite_coordinates() {
let simplex = LabeledSimplexRealization::try_new(
vec![0, 1, 2],
vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
)
.unwrap();
let err = simplex
.try_translated(&ToroidalDomain::try_new([f64::MAX, 1.0]).unwrap(), &[2, 0])
.unwrap_err();
assert_matches!(
err,
LabeledSimplexRealizationError::NonFiniteCoordinate {
vertex_index: 0,
coordinate_index: 0,
coordinate_value: InvalidCoordinateValue::PositiveInfinity,
}
);
}
#[test]
fn periodic_realization_preserves_validated_domain_across_translations() {
let simplex = LabeledSimplexRealization::try_new(
vec![0, 1, 2],
vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
)
.unwrap();
let domain = ToroidalDomain::try_new([2.5, 3.25]).unwrap();
let translated = simplex.try_translated(&domain, &[1, -1]).unwrap();
let expected = [[2.5, -3.25], [3.5, -3.25], [2.5, -2.25]];
assert_eq!(translated.coordinates().len(), expected.len());
for (actual, expected) in translated.coordinates().iter().zip(expected) {
for (actual, expected) in actual.iter().zip(expected) {
assert_abs_diff_eq!(*actual, expected, epsilon = f64::EPSILON);
}
}
assert_eq!(translated.labels(), simplex.labels());
assert!(periodic_simplex_span(&simplex, &domain).is_none());
assert!(periodic_simplex_span(&translated, &domain).is_none());
let restored = translated.try_translated(&domain, &[-1, 1]).unwrap();
assert_eq!(restored, simplex);
}
#[test]
fn translated_realization_requires_only_clone_labels() {
let simplex = LabeledSimplexRealization {
labels: vec![CloneOnlyLabel, CloneOnlyLabel, CloneOnlyLabel]
.into_iter()
.collect(),
coordinates: vec![[0.0, 0.0], [0.5, 0.0], [0.0, 0.5]]
.into_iter()
.collect(),
};
let translated = simplex
.try_translated(&ToroidalDomain::unit(), &[1, 0])
.expect("translation preserves already-validated labels");
assert_eq!(translated.labels().len(), 3);
assert_abs_diff_eq!(translated.coordinates()[1][0], 1.5, epsilon = f64::EPSILON);
}
#[test]
fn crossing_triangles_report_positive_nonshared_witnesses() {
let first = LabeledSimplexRealization::try_new(
vec![0, 1, 2],
vec![[0.0, 0.0], [2.0, 0.0], [0.0, 2.0]],
)
.unwrap();
let second = LabeledSimplexRealization::try_new(
vec![3, 4, 5],
vec![[2.0, 2.0], [1.0, -1.0], [3.0, 2.0]],
)
.unwrap();
let err = validate_simplex_intersection(&first, &second).unwrap_err();
assert_matches!(
err,
SimplexIntersectionFailure::IntersectionOutsideSharedFace { witness, .. }
if witness.first_only_witness.iter().any(|label| [0, 1, 2].contains(label))
&& witness.second_only_witness.iter().any(|label| [3, 4, 5].contains(label))
);
}
#[test]
fn eight_dimensional_intersections_fail_closed() {
let mut coords = vec![[0.0; 8]];
for axis in 0..8 {
let mut point = [0.0; 8];
point[axis] = 1.0;
coords.push(point);
}
let first = LabeledSimplexRealization::try_new(0..9, coords.clone()).unwrap();
let second = LabeledSimplexRealization::try_new(9..18, coords).unwrap();
assert_matches!(
validate_simplex_intersection(&first, &second),
Err(SimplexIntersectionFailure::UnsupportedDimension {
dimension: 8,
max: 7
})
);
assert_matches!(
validate_simplex_intersection(&first, &first),
Err(SimplexIntersectionFailure::UnsupportedDimension {
dimension: 8,
max: 7
})
);
}
#[test]
fn spanning_periodic_simplex_is_detected() {
let simplex = LabeledSimplexRealization::try_new(
vec![0, 1, 2],
vec![[0.0, 0.0], [1.0, 0.0], [0.0, 0.25]],
)
.unwrap();
let span = periodic_simplex_span(&simplex, &ToroidalDomain::unit()).unwrap();
assert_eq!(span.axis(), 0);
assert_abs_diff_eq!(span.span(), 1.0, epsilon = f64::EPSILON);
assert_abs_diff_eq!(span.period(), 1.0, epsilon = f64::EPSILON);
}
#[test]
fn periodic_simplex_span_detects_unrepresentable_coordinate_span() {
let simplex = LabeledSimplexRealization::try_new(
vec![0, 1, 2],
vec![[-f64::MAX, 0.0], [f64::MAX, 0.0], [0.0, 0.25]],
)
.unwrap();
let domain = ToroidalDomain::try_new([f64::MAX, 1.0]).unwrap();
let span = periodic_simplex_span(&simplex, &domain).unwrap();
assert_eq!(span.axis(), 0);
assert!(span.span().is_infinite());
assert_eq!(span.period().to_bits(), f64::MAX.to_bits());
}
}