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