use crate::core::{facet::FacetError, tds::TdsError};
use crate::topology::manifold::ManifoldError;
use thiserror::Error;
#[derive(Clone, Debug, Error, PartialEq)]
#[non_exhaustive]
pub enum TopologyError {
#[error("Failed to build facet incidence map during topology analysis: {source}")]
FacetMapBuild {
#[source]
source: TdsError,
},
#[error("Failed to enumerate boundary facets during topology analysis: {source}")]
BoundaryFacetEnumeration {
#[source]
source: TdsError,
},
#[error("Failed to access boundary facet simplex during topology analysis: {source}")]
BoundaryFacetSimplexAccess {
#[source]
source: FacetError,
},
#[error("Failed to count boundary facets during topology classification: {source}")]
BoundaryFacetCount {
#[source]
source: TdsError,
},
#[error("Failed to classify boundary facets during topology analysis: {source}")]
BoundaryClassification {
#[source]
source: Box<ManifoldError>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TopologyKind {
Euclidean,
Toroidal,
Spherical,
Hyperbolic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToroidalConstructionMode {
PeriodicImagePoint,
Explicit,
}
#[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)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GlobalTopology<const D: usize> {
Euclidean,
Toroidal {
domain: ToroidalDomain<D>,
mode: ToroidalConstructionMode,
},
Spherical,
Hyperbolic,
}
impl<const D: usize> Default for GlobalTopology<D> {
fn default() -> Self {
Self::DEFAULT
}
}
impl<const D: usize> GlobalTopology<D> {
pub const DEFAULT: Self = Self::Euclidean;
pub fn try_toroidal(
domain: [f64; D],
mode: ToroidalConstructionMode,
) -> Result<Self, ToroidalDomainError> {
Ok(Self::Toroidal {
domain: ToroidalDomain::try_new(domain)?,
mode,
})
}
#[must_use]
pub const fn kind(self) -> TopologyKind {
match self {
Self::Euclidean => TopologyKind::Euclidean,
Self::Toroidal { .. } => TopologyKind::Toroidal,
Self::Spherical => TopologyKind::Spherical,
Self::Hyperbolic => TopologyKind::Hyperbolic,
}
}
#[must_use]
pub const fn allows_boundary(self) -> bool {
match self {
Self::Euclidean => true,
Self::Toroidal { .. } | Self::Spherical | Self::Hyperbolic => false,
}
}
#[must_use]
pub const fn is_euclidean(self) -> bool {
matches!(self, Self::Euclidean)
}
#[must_use]
pub const fn is_toroidal(self) -> bool {
matches!(self, Self::Toroidal { .. })
}
#[must_use]
pub const fn is_periodic(self) -> bool {
matches!(
self,
Self::Toroidal {
mode: ToroidalConstructionMode::PeriodicImagePoint,
..
}
)
}
}
pub trait TopologicalSpace {
const DIM: usize;
fn kind(&self) -> TopologyKind;
fn allows_boundary(&self) -> bool;
fn canonicalize_point(&self, coords: &mut [f64]);
fn fundamental_domain(&self) -> Option<&[f64]>;
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use std::assert_matches;
#[test]
fn test_topology_error_display() {
let counting = TopologyError::FacetMapBuild {
source: TdsError::InconsistentDataStructure {
message: "test message".to_string(),
},
};
assert_eq!(
counting.to_string(),
"Failed to build facet incidence map during topology analysis: Internal data structure inconsistency: test message"
);
let classification = TopologyError::BoundaryFacetCount {
source: TdsError::InconsistentDataStructure {
message: "another test".to_string(),
},
};
assert_eq!(
classification.to_string(),
"Failed to count boundary facets during topology classification: Internal data structure inconsistency: another test"
);
}
#[test]
fn test_topology_error_equality() {
let err1 = TopologyError::FacetMapBuild {
source: TdsError::InconsistentDataStructure {
message: "msg".to_string(),
},
};
let err2 = TopologyError::FacetMapBuild {
source: TdsError::InconsistentDataStructure {
message: "msg".to_string(),
},
};
let err3 = TopologyError::FacetMapBuild {
source: TdsError::InconsistentDataStructure {
message: "different".to_string(),
},
};
assert_eq!(err1, err2);
assert_ne!(err1, err3);
assert_ne!(
err1,
TopologyError::BoundaryFacetCount {
source: TdsError::InconsistentDataStructure {
message: "msg".to_string(),
},
}
);
}
#[test]
fn test_topology_kind_debug() {
assert_eq!(format!("{:?}", TopologyKind::Euclidean), "Euclidean");
assert_eq!(format!("{:?}", TopologyKind::Toroidal), "Toroidal");
assert_eq!(format!("{:?}", TopologyKind::Spherical), "Spherical");
assert_eq!(format!("{:?}", TopologyKind::Hyperbolic), "Hyperbolic");
}
#[test]
fn test_toroidal_construction_mode_debug() {
assert_eq!(
format!("{:?}", ToroidalConstructionMode::PeriodicImagePoint),
"PeriodicImagePoint"
);
assert_eq!(
format!("{:?}", ToroidalConstructionMode::Explicit),
"Explicit"
);
}
#[test]
fn test_global_topology_default() {
let default_topo: GlobalTopology<3> = GlobalTopology::default();
assert_eq!(default_topo, GlobalTopology::Euclidean);
assert_eq!(GlobalTopology::<3>::DEFAULT, GlobalTopology::Euclidean);
}
#[test]
fn test_global_topology_kind() {
assert_eq!(
GlobalTopology::<2>::Euclidean.kind(),
TopologyKind::Euclidean
);
assert_eq!(
GlobalTopology::<3>::Spherical.kind(),
TopologyKind::Spherical
);
assert_eq!(
GlobalTopology::<4>::Hyperbolic.kind(),
TopologyKind::Hyperbolic
);
let toroidal = GlobalTopology::<2>::Toroidal {
domain: ToroidalDomain::try_new([1.0, 2.0]).unwrap(),
mode: ToroidalConstructionMode::PeriodicImagePoint,
};
assert_eq!(toroidal.kind(), TopologyKind::Toroidal);
}
#[test]
fn test_global_topology_allows_boundary() {
assert!(GlobalTopology::<3>::Euclidean.allows_boundary());
assert!(!GlobalTopology::<3>::Spherical.allows_boundary());
assert!(!GlobalTopology::<3>::Hyperbolic.allows_boundary());
let toroidal = GlobalTopology::<2>::Toroidal {
domain: ToroidalDomain::try_new([1.0, 1.0]).unwrap(),
mode: ToroidalConstructionMode::PeriodicImagePoint,
};
assert!(!toroidal.allows_boundary());
}
#[test]
fn test_global_topology_is_euclidean() {
assert!(GlobalTopology::<3>::Euclidean.is_euclidean());
assert!(!GlobalTopology::<3>::Spherical.is_euclidean());
assert!(!GlobalTopology::<3>::Hyperbolic.is_euclidean());
let toroidal = GlobalTopology::<2>::Toroidal {
domain: ToroidalDomain::try_new([1.0, 1.0]).unwrap(),
mode: ToroidalConstructionMode::PeriodicImagePoint,
};
assert!(!toroidal.is_euclidean());
}
#[test]
fn test_global_topology_is_toroidal() {
assert!(!GlobalTopology::<3>::Euclidean.is_toroidal());
assert!(!GlobalTopology::<3>::Spherical.is_toroidal());
assert!(!GlobalTopology::<3>::Hyperbolic.is_toroidal());
let toroidal = GlobalTopology::<2>::Toroidal {
domain: ToroidalDomain::try_new([1.0, 1.0]).unwrap(),
mode: ToroidalConstructionMode::PeriodicImagePoint,
};
assert!(toroidal.is_toroidal());
}
#[test]
fn test_global_topology_is_periodic() {
assert!(!GlobalTopology::<3>::Euclidean.is_periodic());
assert!(!GlobalTopology::<3>::Spherical.is_periodic());
assert!(!GlobalTopology::<3>::Hyperbolic.is_periodic());
let periodic = GlobalTopology::<2>::Toroidal {
domain: ToroidalDomain::try_new([1.0, 1.0]).unwrap(),
mode: ToroidalConstructionMode::PeriodicImagePoint,
};
assert!(periodic.is_periodic());
let explicit = GlobalTopology::<2>::Toroidal {
domain: ToroidalDomain::try_new([1.0, 1.0]).unwrap(),
mode: ToroidalConstructionMode::Explicit,
};
assert!(!explicit.is_periodic());
}
#[test]
fn test_global_topology_equality() {
let topo1 = GlobalTopology::<3>::Euclidean;
let topo2 = GlobalTopology::<3>::Euclidean;
let topo3 = GlobalTopology::<3>::Spherical;
assert_eq!(topo1, topo2);
assert_ne!(topo1, topo3);
let toroidal1 = GlobalTopology::<2>::Toroidal {
domain: ToroidalDomain::try_new([1.0, 2.0]).unwrap(),
mode: ToroidalConstructionMode::PeriodicImagePoint,
};
let toroidal2 = GlobalTopology::<2>::Toroidal {
domain: ToroidalDomain::try_new([1.0, 2.0]).unwrap(),
mode: ToroidalConstructionMode::PeriodicImagePoint,
};
let toroidal3 = GlobalTopology::<2>::Toroidal {
domain: ToroidalDomain::try_new([1.0, 2.0]).unwrap(),
mode: ToroidalConstructionMode::Explicit,
};
assert_eq!(toroidal1, toroidal2);
assert_ne!(toroidal1, toroidal3);
}
#[test]
fn test_global_topology_debug() {
assert_eq!(format!("{:?}", GlobalTopology::<3>::Euclidean), "Euclidean");
let toroidal = GlobalTopology::<2>::Toroidal {
domain: ToroidalDomain::try_new([1.5, 2.5]).unwrap(),
mode: ToroidalConstructionMode::PeriodicImagePoint,
};
let debug_str = format!("{toroidal:?}");
assert!(debug_str.contains("Toroidal"));
assert!(debug_str.contains("domain"));
assert!(debug_str.contains("mode"));
}
#[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 domain = ToroidalDomain::<3>::try_from([1.0, 2.0, 4.0]).unwrap();
assert_relative_eq!(domain.periods()[0], 1.0);
assert_relative_eq!(domain.periods()[1], 2.0);
assert_relative_eq!(domain.periods()[2], 4.0);
let periods = domain.into_periods();
assert_relative_eq!(periods[0], 1.0);
assert_relative_eq!(periods[1], 2.0);
assert_relative_eq!(periods[2], 4.0);
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()
);
}
#[test]
fn test_global_topology_try_toroidal_parses_domain() {
let topology =
GlobalTopology::try_toroidal([1.0, 2.0], ToroidalConstructionMode::PeriodicImagePoint)
.unwrap();
assert_eq!(topology.kind(), TopologyKind::Toroidal);
assert!(topology.is_periodic());
let err = GlobalTopology::<2>::try_toroidal(
[0.0, 2.0],
ToroidalConstructionMode::PeriodicImagePoint,
)
.unwrap_err();
assert_matches!(
err,
ToroidalDomainError::InvalidPeriod { axis: 0, period }
if period.abs() < f64::EPSILON
);
}
}