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::E1901).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 fmt::Display for SpatialError {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            Self::Memory(error) => error.fmt(f),
135            Self::Cancelled => f.write_str("spatial operation cancelled"),
136            Self::InvalidCutoff => f.write_str("cutoff must be finite and non-negative"),
137            Self::AtomOutOfBounds(atom) => write!(f, "atom {atom} is outside the coordinate array"),
138            Self::InvalidCell => f.write_str("unit cell is degenerate or non-finite"),
139            Self::StaleNeighborList => {
140                f.write_str("positions moved beyond the neighbour-list displacement skin")
141            }
142            Self::InvalidOption(option) => write!(f, "invalid spatial option: {option}"),
143            Self::PeriodicImageLimitExceeded { required, maximum } => write!(
144                f,
145                "periodic k-d query requires {required} images but the configured limit is {maximum}"
146            ),
147            Self::NumericRangeExceeded => {
148                f.write_str("a derived spatial value exceeds its representable numeric range")
149            }
150            Self::WorkerPanicked => f.write_str("a spatial worker thread panicked"),
151        }
152    }
153}
154
155impl std::error::Error for SpatialError {}
156
157impl fmt::Display for SpatialOption {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        f.write_str(match self {
160            Self::BruteForcePairLimit => "brute_force_pair_limit",
161            Self::KdTargetMinimum => "kd_target_minimum",
162            Self::KdQueryRatio => "kd_query_ratio",
163            Self::UnresolvedBackend => "unresolved_backend",
164            Self::NeighborSkinRatio => "neighbor_skin.cutoff_ratio",
165            Self::NeighborSkinMinimum => "neighbor_skin.minimum",
166            Self::MaximumCellCount => "cell_grid.maximum_cell_count",
167            Self::CellGrowthFactor => "cell_grid.edge_growth_factor",
168            Self::KdPeriodicImageLimit => "kd_periodic.maximum_image_count",
169        })
170    }
171}
172
173impl From<molframe_core::MemoryBudgetError> for SpatialError {
174    fn from(error: molframe_core::MemoryBudgetError) -> Self {
175        Self::Memory(error)
176    }
177}
178
179#[cfg(test)]
180#[path = "types_tests.rs"]
181mod tests;