Skip to main content

molframe_spatial/model/
types.rs

1//! Public spatial query values.
2
3use molframe_core::{Code, Diagnostic};
4use std::fmt;
5
6/// A deterministic spatial implementation choice.
7#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
8#[non_exhaustive]
9pub enum SpatialBackend {
10    /// Direct comparison, for small one-shot workloads.
11    BruteForce,
12    /// Contiguous fixed-radius grid.
13    CellList,
14    /// Balanced static k-dimensional tree.
15    KdTree,
16    /// Verlet candidate list with a displacement skin.
17    NeighborList,
18    /// Select from workload shape.
19    Auto,
20}
21
22/// One unique unordered atom pair within a cutoff.
23#[derive(Clone, Copy, PartialEq, Debug)]
24pub struct NeighborPair {
25    /// Lower atom index.
26    pub first: u32,
27    /// Higher atom index.
28    pub second: u32,
29    /// Squared separation in ångström squared.
30    pub distance_squared: f32,
31}
32
33impl NeighborPair {
34    pub(crate) fn new(left: u32, right: u32, distance_squared: f32) -> Self {
35        Self {
36            first: left.min(right),
37            second: left.max(right),
38            distance_squared,
39        }
40    }
41}
42
43/// Why a spatial query could not be evaluated.
44#[derive(Clone, Copy, PartialEq, Eq, Debug)]
45#[non_exhaustive]
46pub enum SpatialError {
47    /// Shared execution memory cannot admit the complete spatial workspace.
48    Memory(molframe_core::MemoryBudgetError),
49    /// Cooperative cancellation stopped the spatial operation.
50    Cancelled,
51    /// The cutoff was negative or non-finite.
52    InvalidCutoff,
53    /// A selection named an atom outside the coordinate array.
54    AtomOutOfBounds(u32),
55    /// Cell parameters do not define an invertible three-dimensional box.
56    InvalidCell,
57    /// Positions moved beyond the displacement skin used to build a list.
58    StaleNeighborList,
59    /// A public planning option is invalid.
60    InvalidOption(SpatialOption),
61    /// A periodic k-d query exceeds its explicit image budget.
62    PeriodicImageLimitExceeded {
63        /// Images required for an exact query.
64        required: usize,
65        /// Maximum images permitted by the profile.
66        maximum: usize,
67    },
68    /// A derived coordinate or index cannot be represented by the public type.
69    NumericRangeExceeded,
70    /// A scoped worker thread panicked during a parallel search.
71    WorkerPanicked,
72}
73
74/// Identifies an invalid field in a spatial planning profile.
75#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
76#[non_exhaustive]
77pub enum SpatialOption {
78    /// Direct-comparison workload threshold.
79    BruteForcePairLimit,
80    /// Minimum target count for a k-d tree.
81    KdTargetMinimum,
82    /// Target-to-query ratio for a k-d tree.
83    KdQueryRatio,
84    /// Automatic planning did not resolve to a concrete backend.
85    UnresolvedBackend,
86    /// Cutoff fraction used to derive a neighbour skin.
87    NeighborSkinRatio,
88    /// Minimum or absolute neighbour skin.
89    NeighborSkinMinimum,
90    /// Maximum allocated cell count.
91    MaximumCellCount,
92    /// Cell-edge coarsening multiplier.
93    CellGrowthFactor,
94    /// Maximum periodic k-d query image count.
95    KdPeriodicImageLimit,
96}
97
98impl SpatialError {
99    /// Converts the backend error into molframe's registered diagnostic model.
100    #[must_use]
101    pub fn into_diagnostic(self) -> Diagnostic {
102        match self {
103            Self::Memory(error) => {
104                Diagnostic::new(Code::E7001).with_context("execution_memory", error.to_string())
105            }
106            Self::Cancelled => {
107                Diagnostic::new(Code::E1904).with_context("spatial_operation", "cancelled")
108            }
109            Self::InvalidCutoff => Diagnostic::new(Code::E4002),
110            Self::AtomOutOfBounds(atom) => {
111                Diagnostic::new(Code::E6009).with_context("atom", atom.to_string())
112            }
113            Self::InvalidCell => Diagnostic::new(Code::E5004),
114            Self::StaleNeighborList => Diagnostic::new(Code::E9001),
115            Self::InvalidOption(option) => {
116                Diagnostic::new(Code::E4002).with_context("spatial_option", option.to_string())
117            }
118            Self::PeriodicImageLimitExceeded { required, maximum } => Diagnostic::new(Code::E4002)
119                .with_context("required_periodic_images", required.to_string())
120                .with_context("maximum_periodic_images", maximum.to_string()),
121            Self::NumericRangeExceeded => {
122                Diagnostic::new(Code::E4002).with_context("spatial_numeric_range", "exceeded")
123            }
124            Self::WorkerPanicked => {
125                Diagnostic::new(Code::E4002).with_context("spatial_worker", "panicked")
126            }
127        }
128    }
129}
130
131impl From<SpatialError> for Diagnostic {
132    fn from(error: SpatialError) -> Self {
133        error.into_diagnostic().with_message(error.to_string())
134    }
135}
136
137impl From<&SpatialError> for Diagnostic {
138    fn from(error: &SpatialError) -> Self {
139        Self::from(*error)
140    }
141}
142
143impl fmt::Display for SpatialError {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Self::Memory(error) => error.fmt(f),
147            Self::Cancelled => f.write_str("spatial operation cancelled"),
148            Self::InvalidCutoff => f.write_str("cutoff must be finite and non-negative"),
149            Self::AtomOutOfBounds(atom) => write!(f, "atom {atom} is outside the coordinate array"),
150            Self::InvalidCell => f.write_str("unit cell is degenerate or non-finite"),
151            Self::StaleNeighborList => {
152                f.write_str("positions moved beyond the neighbour-list displacement skin")
153            }
154            Self::InvalidOption(option) => write!(f, "invalid spatial option: {option}"),
155            Self::PeriodicImageLimitExceeded { required, maximum } => write!(
156                f,
157                "periodic k-d query requires {required} images but the configured limit is {maximum}"
158            ),
159            Self::NumericRangeExceeded => {
160                f.write_str("a derived spatial value exceeds its representable numeric range")
161            }
162            Self::WorkerPanicked => f.write_str("a spatial worker thread panicked"),
163        }
164    }
165}
166
167impl std::error::Error for SpatialError {}
168
169impl fmt::Display for SpatialOption {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.write_str(match self {
172            Self::BruteForcePairLimit => "brute_force_pair_limit",
173            Self::KdTargetMinimum => "kd_target_minimum",
174            Self::KdQueryRatio => "kd_query_ratio",
175            Self::UnresolvedBackend => "unresolved_backend",
176            Self::NeighborSkinRatio => "neighbor_skin.cutoff_ratio",
177            Self::NeighborSkinMinimum => "neighbor_skin.minimum",
178            Self::MaximumCellCount => "cell_grid.maximum_cell_count",
179            Self::CellGrowthFactor => "cell_grid.edge_growth_factor",
180            Self::KdPeriodicImageLimit => "kd_periodic.maximum_image_count",
181        })
182    }
183}
184
185impl From<molframe_core::MemoryBudgetError> for SpatialError {
186    fn from(error: molframe_core::MemoryBudgetError) -> Self {
187        Self::Memory(error)
188    }
189}
190
191#[cfg(test)]
192#[path = "types_tests.rs"]
193mod tests;