use thiserror::Error;
#[derive(Clone, Copy, Debug, Error, PartialEq)]
#[non_exhaustive]
pub enum ToroidalDomainError {
#[error("Invalid toroidal period {period:?} on axis {axis}; expected finite value > 0")]
InvalidPeriod {
axis: usize,
period: f64,
},
}
#[must_use]
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ToroidalDomain<const D: usize> {
periods: [f64; D],
}
impl<const D: usize> ToroidalDomain<D> {
pub fn try_new(periods: [f64; D]) -> Result<Self, ToroidalDomainError> {
for (axis, period) in periods.iter().copied().enumerate() {
if !period.is_finite() || period <= 0.0 {
return Err(ToroidalDomainError::InvalidPeriod { axis, period });
}
}
Ok(Self { periods })
}
pub const fn unit() -> Self {
Self { periods: [1.0; D] }
}
#[must_use]
pub const fn periods(&self) -> &[f64; D] {
&self.periods
}
#[must_use]
pub fn period(&self, axis: usize) -> Option<f64> {
self.periods.get(axis).copied()
}
#[must_use]
pub const fn into_periods(self) -> [f64; D] {
self.periods
}
}
impl<const D: usize> TryFrom<[f64; D]> for ToroidalDomain<D> {
type Error = ToroidalDomainError;
fn try_from(value: [f64; D]) -> Result<Self, Self::Error> {
Self::try_new(value)
}
}
#[inline]
pub(crate) fn wrap_coordinate(value: f64, period: f64) -> f64 {
let wrapped = value.rem_euclid(period);
if wrapped >= period { 0.0 } else { wrapped }
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use super::*;
#[test]
fn test_toroidal_domain_try_new_rejects_invalid_periods() {
let zero = ToroidalDomain::<2>::try_new([1.0, 0.0]).unwrap_err();
assert_matches!(
zero,
ToroidalDomainError::InvalidPeriod { axis: 1, period }
if period.abs() < f64::EPSILON
);
let negative = ToroidalDomain::<2>::try_new([-1.0, 1.0]).unwrap_err();
assert_matches!(
negative,
ToroidalDomainError::InvalidPeriod { axis: 0, period }
if period < 0.0
);
let nan = ToroidalDomain::<2>::try_new([f64::NAN, 1.0]).unwrap_err();
assert_matches!(
nan,
ToroidalDomainError::InvalidPeriod { axis: 0, period }
if period.is_nan()
);
let infinite = ToroidalDomain::<2>::try_new([1.0, f64::INFINITY]).unwrap_err();
assert_matches!(
infinite,
ToroidalDomainError::InvalidPeriod { axis: 1, period }
if period.is_infinite()
);
}
#[test]
fn test_toroidal_domain_try_from_and_into_periods_preserve_validation() {
let periods = [1.0, 2.0, 4.0];
let domain = ToroidalDomain::<3>::try_from(periods).unwrap();
let expected_bits = periods.map(f64::to_bits);
assert_eq!(domain.periods().map(f64::to_bits), expected_bits);
assert_eq!(domain.into_periods().map(f64::to_bits), expected_bits);
let invalid = ToroidalDomain::<3>::try_from([1.0, f64::NEG_INFINITY, 4.0]).unwrap_err();
assert_matches!(
invalid,
ToroidalDomainError::InvalidPeriod { axis: 1, period }
if period.is_infinite() && period.is_sign_negative()
);
}
}