1use crate::brute::finite;
4use crate::{
5 CellList, KdTree, NeighborPair, PeriodicBox, SpatialBackend, SpatialSearchOptions,
6 pairs_within_with_options,
7};
8use molframe_core::ExecutionContext;
9use molframe_core::contract::AnalysisPolicy;
10use molframe_core::diagnostic::{Code, Diagnostic};
11use molframe_core::selection::AtomSelection;
12use molframe_core::structure::Structure;
13use std::collections::{HashMap, hash_map::Entry};
14use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
15
16mod dispatch;
17mod matching;
18mod visit;
19use matching::{
20 atom_out_of_bounds, backend_key, cacheable_backend, collect_selection_indices, collect_within,
21 index_cutoff_key, mark_pair_match, periodic_box, radial_squared, select_iso_layer,
22 spatial_diagnostic, update_nearest_distance, validate_shell, z_matches,
23};
24
25const MAX_CACHED_INDICES: usize = 64;
31
32#[derive(Debug)]
34pub struct StructureSpatial<'a> {
35 structure: &'a Structure,
36 context: &'a ExecutionContext,
37 options: SpatialSearchOptions,
38 periodic: Option<PeriodicBox>,
39 cache: RwLock<HashMap<IndexKey, Arc<CachedIndex<'a>>>>,
40}
41
42#[derive(Debug, PartialEq, Eq, Hash)]
47struct IndexKey {
48 backend: u8,
49 cutoff: u32,
50 selection: Box<[u32]>,
51}
52
53#[derive(Debug)]
55enum CachedIndex<'a> {
56 Cell(CellList<'a>),
57 Kd(KdTree<'a>),
58}
59
60impl<'a> StructureSpatial<'a> {
61 pub fn new(
67 structure: &'a Structure,
68 policy: &AnalysisPolicy,
69 backend: SpatialBackend,
70 context: &'a ExecutionContext,
71 ) -> Result<Self, Diagnostic> {
72 Self::new_with_options(
73 structure,
74 policy,
75 SpatialSearchOptions::with_backend(backend),
76 context,
77 )
78 }
79
80 pub fn new_with_options(
86 structure: &'a Structure,
87 policy: &AnalysisPolicy,
88 options: SpatialSearchOptions,
89 context: &'a ExecutionContext,
90 ) -> Result<Self, Diagnostic> {
91 options.validate().map_err(spatial_diagnostic)?;
92 let periodic = periodic_box(structure, policy)?;
93
94 Ok(Self {
95 structure,
96 context,
97 options,
98 periodic,
99 cache: RwLock::new(HashMap::new()),
100 })
101 }
102
103 #[must_use]
105 pub fn cached_index_count(&self) -> usize {
106 self.read_cache().len()
107 }
108
109 #[inline]
111 fn positions(&self) -> &[[f32; 3]] {
112 self.structure.positions()
113 }
114
115 #[inline]
117 fn position(&self, atom: u32) -> Result<[f32; 3], Diagnostic> {
118 let index = usize::try_from(atom).map_err(|_| atom_out_of_bounds(atom))?;
119
120 self.positions()
121 .get(index)
122 .copied()
123 .ok_or_else(|| atom_out_of_bounds(atom))
124 }
125
126 fn radial(
131 &self,
132 universe: &AtomSelection,
133 centre: [f32; 3],
134 inner: f32,
135 outer: f32,
136 z: Option<(f32, f32)>,
137 ) -> Result<AtomSelection, Diagnostic> {
138 validate_shell(inner, outer)?;
139
140 let inner_squared = inner * inner;
141 let outer_squared = outer * outer;
142 let cylindrical = z.is_some();
143 let mut selected = Vec::with_capacity(matching::selection_capacity(universe));
144
145 for atom in universe {
146 let position = self.position(atom)?;
147
148 if !finite(position) {
149 continue;
150 }
151
152 let displacement = self.displacement(centre, position);
153
154 if !z_matches(displacement[2], z) {
155 continue;
156 }
157
158 let squared = radial_squared(displacement, cylindrical);
159
160 if squared >= inner_squared && squared <= outer_squared {
161 selected.push(atom);
162 }
163 }
164
165 Ok(AtomSelection::from_sorted(selected))
166 }
167
168 #[inline]
170 fn displacement(&self, left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
171 match &self.periodic {
172 Some(periodic) => periodic.displacement(left, right),
173 None => [right[0] - left[0], right[1] - left[1], right[2] - left[2]],
174 }
175 }
176
177 fn centre(&self, target: &AtomSelection) -> Result<[f32; 3], Diagnostic> {
184 if target.is_empty() {
185 return Err(Diagnostic::new(Code::E5003));
186 }
187
188 let mut sum = [0.0f64; 3];
189 let mut count = 0u32;
190
191 for atom in target {
192 let position = self.position(atom)?;
193
194 if !finite(position) {
195 continue;
196 }
197
198 sum[0] += f64::from(position[0]);
199 sum[1] += f64::from(position[1]);
200 sum[2] += f64::from(position[2]);
201
202 count = count
203 .checked_add(1)
204 .ok_or_else(|| Diagnostic::new(Code::E3001))?;
205 }
206
207 if count == 0 {
208 return Err(Diagnostic::new(Code::E5003));
209 }
210
211 let divisor = f64::from(count);
212
213 let convert = |value: f64| {
214 crate::numeric::finite_f32(value / divisor).ok_or_else(|| Diagnostic::new(Code::E5003))
215 };
216
217 Ok([convert(sum[0])?, convert(sum[1])?, convert(sum[2])?])
218 }
219
220 fn radial_from_target(
224 &self,
225 universe: &AtomSelection,
226 target: &AtomSelection,
227 inner: f32,
228 outer: f32,
229 z: Option<(f32, f32)>,
230 ) -> Result<AtomSelection, Diagnostic> {
231 let centre = self.centre(target)?;
232 self.radial(universe, centre, inner, outer, z)
233 }
234
235 fn iso_layer(
240 &self,
241 universe: &AtomSelection,
242 target: &AtomSelection,
243 inner: f32,
244 outer: f32,
245 ) -> Result<AtomSelection, Diagnostic> {
246 validate_shell(inner, outer)?;
247
248 let mut nearest = vec![f32::INFINITY; self.positions().len()];
249 let mut reduction_error = None;
250 self.for_each_pair(universe, target, outer, |pair| {
251 if reduction_error.is_none()
252 && let Err(error) = update_nearest_distance(&mut nearest, universe, target, pair)
253 {
254 reduction_error = Some(error);
255 }
256 })?;
257 if let Some(error) = reduction_error {
258 return Err(error);
259 }
260
261 select_iso_layer(universe, target, &nearest, inner * inner, outer * outer)
262 }
263
264 fn within(
269 &self,
270 query: &AtomSelection,
271 target: &AtomSelection,
272 cutoff: f32,
273 ) -> Result<AtomSelection, Diagnostic> {
274 let mut matched = vec![false; self.positions().len()];
275 let mut reduction_error = None;
276 self.for_each_pair(query, target, cutoff, |pair| {
277 if reduction_error.is_none()
278 && let Err(error) = mark_pair_match(&mut matched, query, target, pair)
279 {
280 reduction_error = Some(error);
281 }
282 })?;
283 if let Some(error) = reduction_error {
284 return Err(error);
285 }
286
287 collect_within(query, target, &matched)
288 }
289
290 pub fn pairs_with_backend(
302 &self,
303 query: &AtomSelection,
304 target: &AtomSelection,
305 cutoff: f32,
306 requested_backend: SpatialBackend,
307 ) -> Result<Vec<NeighborPair>, Diagnostic> {
308 let requested = SpatialSearchOptions {
309 backend: requested_backend,
310 ..self.options
311 };
312 let backend =
313 cacheable_backend(requested, query.len(), target.len()).map_err(spatial_diagnostic)?;
314
315 match backend {
316 SpatialBackend::CellList | SpatialBackend::KdTree => {
317 self.cached_pairs(query, target, cutoff, backend)
318 }
319 _ => {
320 let options = SpatialSearchOptions {
321 backend,
322 ..self.options
323 };
324
325 pairs_within_with_options(
326 self.positions(),
327 query,
328 target,
329 cutoff,
330 options,
331 self.periodic.as_ref(),
332 self.context,
333 )
334 .map_err(spatial_diagnostic)
335 }
336 }
337 }
338
339 fn cached_pairs(
344 &self,
345 query: &AtomSelection,
346 target: &AtomSelection,
347 cutoff: f32,
348 backend: SpatialBackend,
349 ) -> Result<Vec<NeighborPair>, Diagnostic> {
350 let target_indices = collect_selection_indices(target);
351
352 let key = IndexKey {
353 backend: backend_key(backend),
354 cutoff: index_cutoff_key(backend, cutoff),
355 selection: target_indices.into_boxed_slice(),
356 };
357
358 let index = self.cached_index(key, backend, cutoff)?;
359 let query_indices = collect_selection_indices(query);
360
361 match index.as_ref() {
362 CachedIndex::Cell(index) => index
363 .pairs(&query_indices, cutoff)
364 .map_err(spatial_diagnostic),
365 CachedIndex::Kd(index) => index
366 .pairs(&query_indices, cutoff)
367 .map_err(spatial_diagnostic),
368 }
369 }
370
371 fn cached_index(
377 &self,
378 key: IndexKey,
379 backend: SpatialBackend,
380 cutoff: f32,
381 ) -> Result<Arc<CachedIndex<'a>>, Diagnostic> {
382 {
383 let cache = self.read_cache();
384
385 if let Some(index) = cache.get(&key) {
386 return Ok(Arc::clone(index));
387 }
388 }
389
390 let built = Arc::new(self.build_cached_index(backend, key.selection.as_ref(), cutoff)?);
391
392 let mut cache = self.write_cache();
393 let can_retain = cache.len() < MAX_CACHED_INDICES;
394
395 match cache.entry(key) {
396 Entry::Occupied(entry) => Ok(Arc::clone(entry.get())),
397 Entry::Vacant(entry) if can_retain => {
398 entry.insert(Arc::clone(&built));
399 Ok(built)
400 }
401 Entry::Vacant(_) => Ok(built),
402 }
403 }
404
405 fn build_cached_index(
410 &self,
411 backend: SpatialBackend,
412 targets: &[u32],
413 cutoff: f32,
414 ) -> Result<CachedIndex<'a>, Diagnostic> {
415 let positions: &'a [[f32; 3]] = self.structure.positions();
416
417 match backend {
418 SpatialBackend::CellList => CellList::build_with_options(
419 positions,
420 targets,
421 cutoff,
422 self.periodic,
423 self.options.cell_grid,
424 )
425 .map(CachedIndex::Cell)
426 .map_err(spatial_diagnostic),
427 SpatialBackend::KdTree => KdTree::build_with_options(
428 positions,
429 targets,
430 self.periodic,
431 self.options.kd_periodic,
432 )
433 .map(CachedIndex::Kd)
434 .map_err(spatial_diagnostic),
435 _ => Err(Diagnostic::new(Code::E9001)),
436 }
437 }
438
439 fn read_cache(&self) -> RwLockReadGuard<'_, HashMap<IndexKey, Arc<CachedIndex<'a>>>> {
444 match self.cache.read() {
445 Ok(cache) => cache,
446 Err(poisoned) => poisoned.into_inner(),
447 }
448 }
449
450 fn write_cache(&self) -> RwLockWriteGuard<'_, HashMap<IndexKey, Arc<CachedIndex<'a>>>> {
455 match self.cache.write() {
456 Ok(cache) => cache,
457 Err(poisoned) => poisoned.into_inner(),
458 }
459 }
460}
461
462#[cfg(test)]
463#[path = "resolver_tests.rs"]
464mod tests;