#![forbid(unsafe_code)]
use crate::core::collections::{MAX_PRACTICAL_DIMENSION_SIZE, SmallBuffer};
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_linear_program,
shared_face_fast_confinement,
};
use thiserror::Error;
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,
periods: &[f64; D],
shift: &[i32; D],
) -> Result<Self, LabeledSimplexRealizationError>
where
L: Clone,
{
validate_periods(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,
},
#[error(transparent)]
InvalidPeriodicDomainPeriod {
#[from]
source: PeriodicSimplexSpanError,
},
}
#[derive(Clone, Debug, Error, PartialEq)]
#[non_exhaustive]
pub enum PeriodicSimplexSpanError {
#[error("non-finite periodic period at axis {axis}: {period}")]
NonFinitePeriod {
axis: usize,
period: InvalidCoordinateValue,
},
#[error("non-positive periodic period at axis {axis}: {period}")]
NonPositivePeriod {
axis: usize,
period: f64,
},
}
#[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("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
})
}
pub fn try_periodic_simplex_span<L, const D: usize>(
simplex: &LabeledSimplexRealization<L, D>,
periods: &[f64; D],
) -> Result<Option<PeriodicSimplexSpan>, PeriodicSimplexSpanError> {
validate_periods(periods)?;
for (axis, &period) in 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 Ok(Some(PeriodicSimplexSpan { axis, span, period }));
}
}
Ok(None)
}
fn validate_periods<const D: usize>(periods: &[f64; D]) -> Result<(), PeriodicSimplexSpanError> {
for (axis, &period) in periods.iter().enumerate() {
if !period.is_finite() {
return Err(PeriodicSimplexSpanError::NonFinitePeriod {
axis,
period: InvalidCoordinateValue::from_debug(&period),
});
}
if period <= 0.0 {
return Err(PeriodicSimplexSpanError::NonPositivePeriod { axis, period });
}
}
Ok(())
}
pub fn validate_simplex_realizations_intersect_only_in_shared_faces<L, const D: usize>(
first: &LabeledSimplexRealization<L, D>,
second: &LabeledSimplexRealization<L, D>,
) -> Result<(), SimplexIntersectionFailure<L>>
where
L: Clone + Eq,
{
let shared_labels = shared_labels(first, second);
if shared_face_fast_confinement(first, second, &shared_labels) {
return Ok(());
}
let basis_orientation = realization_orientation(first);
if basis_orientation == Some(Orientation::DEGENERATE) {
return Err(SimplexIntersectionFailure::SingularBarycentricBasis);
}
if let Some(orientation) = basis_orientation
&& (simplex_is_strictly_outside_a_facet(first, second, orientation)
|| realization_orientation(second).is_some_and(|second_orientation| {
second_orientation != Orientation::DEGENERATE
&& simplex_is_strictly_outside_a_facet(second, first, second_orientation)
})
|| intersection_is_confined_by_orientation(first, second, &shared_labels, orientation))
{
return Ok(());
}
match intersection_via_linear_program(
first,
second,
&shared_labels,
basis_orientation.is_none(),
) {
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 super::*;
use approx::assert_abs_diff_eq;
use std::assert_matches;
#[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_realizations_intersect_only_in_shared_faces(&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(&[f64::MAX, 1.0], &[2, 0])
.unwrap_err();
assert_matches!(
err,
LabeledSimplexRealizationError::NonFiniteCoordinate {
vertex_index: 0,
coordinate_index: 0,
coordinate_value: InvalidCoordinateValue::PositiveInfinity,
}
);
}
#[test]
fn translated_realization_rejects_invalid_periods() {
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(&[1.0, -1.0], &[0, 1]).unwrap_err();
assert_matches!(
err,
LabeledSimplexRealizationError::InvalidPeriodicDomainPeriod {
source: PeriodicSimplexSpanError::NonPositivePeriod {
axis: 1,
period: -1.0,
},
}
);
}
#[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(&[1.0, 1.0], &[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_realizations_intersect_only_in_shared_faces(&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 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 = try_periodic_simplex_span(&simplex, &[1.0, 1.0])
.unwrap()
.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_rejects_invalid_periods() {
let simplex = LabeledSimplexRealization::try_new(
vec![0, 1, 2],
vec![[0.0, 0.0], [0.5, 0.0], [0.0, 0.25]],
)
.unwrap();
let non_finite = try_periodic_simplex_span(&simplex, &[f64::NAN, 1.0]).unwrap_err();
assert_matches!(
non_finite,
PeriodicSimplexSpanError::NonFinitePeriod {
axis: 0,
period: InvalidCoordinateValue::Nan,
}
);
let non_positive = try_periodic_simplex_span(&simplex, &[1.0, 0.0]).unwrap_err();
assert_matches!(
non_positive,
PeriodicSimplexSpanError::NonPositivePeriod {
axis: 1,
period: 0.0,
}
);
}
}