Skip to main content

molframe_core/selection/query/
representation.rs

1//! Selection shapes, construction, membership and iteration.
2
3use crate::column::{BitVec, Ones};
4use smallvec::SmallVec;
5use std::ops::Range;
6
7/// A set of atoms, addressed by position in the structure's flat atom order.
8///
9/// # Examples
10///
11/// ```
12/// use molframe_core::AtomSelection;
13///
14/// let chain = AtomSelection::range(0..100);
15/// let metals = AtomSelection::from_sorted(vec![5, 50, 500]);
16/// assert_eq!(chain.intersect(&metals).len(), 2);
17/// ```
18#[derive(Clone, PartialEq, Eq, Debug, Default)]
19pub enum AtomSelection {
20    /// No atoms.
21    #[default]
22    Empty,
23    /// Every atom of a structure with this many.
24    All(u32),
25    /// One contiguous run.
26    Range(Range<u32>),
27    /// Several contiguous runs, ascending and disjoint.
28    Ranges(SmallVec<[Range<u32>; 4]>),
29    /// Scattered positions, ascending and unique.
30    Sparse(Vec<u32>),
31    /// A bit per position, for a set dense enough to make that cheaper.
32    Dense(BitVec),
33}
34
35const DENSE_FRACTION: u32 = 16;
36
37impl AtomSelection {
38    /// A selection covering one contiguous run.
39    #[must_use]
40    pub fn range(atoms: Range<u32>) -> Self {
41        if atoms.start >= atoms.end {
42            Self::Empty
43        } else {
44            Self::Range(atoms)
45        }
46    }
47
48    /// Chooses the cheapest shape for ascending unique positions.
49    ///
50    /// # Panics
51    /// Panics if `positions` is not ascending and unique, or if its storage
52    /// exceeds the platform's addressable range. Collect arbitrary positions
53    /// into `AtomSelection` to sort and deduplicate them first.
54    #[must_use]
55    pub fn from_sorted(positions: Vec<u32>) -> Self {
56        let (Some(first), Some(last)) = (positions.first().copied(), positions.last().copied())
57        else {
58            return Self::Empty;
59        };
60        let span = u64::from(last - first) + 1;
61        if last < u32::MAX && span == positions.len() as u64 {
62            return Self::Range(first..last + 1);
63        }
64        if let Some(runs) = inline_runs(&positions) {
65            let covered: u64 = runs.iter().map(|run| u64::from(run.end - run.start)).sum();
66            if covered == positions.len() as u64 && runs.len() * 2 < positions.len() {
67                return Self::Ranges(runs);
68            }
69        }
70        if let Some(mask_len) = last.checked_add(1)
71            && positions.len() as u64 * u64::from(DENSE_FRACTION) >= u64::from(mask_len)
72        {
73            return Self::Dense(mask_from_positions(mask_len, &positions));
74        }
75        Self::Sparse(positions)
76    }
77
78    /// The number of atoms selected.
79    ///
80    /// # Panics
81    /// Panics if manually constructed storage violates normalized `u32` ranges.
82    #[must_use]
83    pub fn len(&self) -> u64 {
84        selection_len(self)
85    }
86
87    /// Returns true when nothing is selected.
88    #[must_use]
89    pub fn is_empty(&self) -> bool {
90        self.len() == 0
91    }
92
93    /// Returns true when `atom` is selected.
94    #[must_use]
95    pub fn contains(&self, atom: u32) -> bool {
96        match self {
97            Self::Empty => false,
98            Self::All(count) => atom < *count,
99            Self::Range(run) => run.contains(&atom),
100            Self::Ranges(runs) => {
101                let candidate = runs.partition_point(|run| run.end <= atom);
102                runs.get(candidate).is_some_and(|run| run.contains(&atom))
103            }
104            Self::Sparse(positions) => positions.binary_search(&atom).is_ok(),
105            Self::Dense(mask) => mask.test(atom),
106        }
107    }
108
109    /// One past the largest position this selection could name.
110    ///
111    /// Derived from the shape rather than the positions, so a whole-structure
112    /// selection answers from its own count and a run from its end. A caller
113    /// that has to prove every position is inside a structure compares this once
114    /// instead of asking about each atom — which matters because the cheapest
115    /// selections, `All` and `Empty`, are exactly the ones a per-atom walk would
116    /// read as a full traversal of the structure.
117    ///
118    /// The bound is exact: no position in the selection reaches it, and every
119    /// shape reports the smallest bound that says so.
120    #[must_use]
121    pub fn position_bound(&self) -> u32 {
122        match self {
123            Self::Empty => 0,
124            Self::All(count) => *count,
125            Self::Range(run) => run.end,
126            Self::Ranges(runs) => match runs.last() {
127                Some(run) => run.end,
128                None => 0,
129            },
130            Self::Sparse(positions) => match positions.last() {
131                Some(last) => last.saturating_add(1),
132                None => 0,
133            },
134            Self::Dense(mask) => mask.len(),
135        }
136    }
137
138    /// The selected positions, ascending.
139    ///
140    /// A concrete iterator rather than a boxed one: the boxed form cost a heap
141    /// allocation per call and a virtual call per position, which prevented the
142    /// consuming loop from inlining anything. Selection walks are the innermost
143    /// loop of every spatial and analysis pass.
144    #[must_use]
145    pub fn iter(&self) -> SelectionIter<'_> {
146        match self {
147            Self::Empty => SelectionIter::Run(0..0),
148            Self::All(count) => SelectionIter::Run(0..*count),
149            Self::Range(run) => SelectionIter::Run(run.start..run.end),
150            Self::Ranges(runs) => SelectionIter::Runs {
151                runs,
152                next: 0,
153                current: 0..0,
154            },
155            Self::Sparse(positions) => SelectionIter::Sparse(positions.iter()),
156            Self::Dense(mask) => SelectionIter::Dense(mask.ones()),
157        }
158    }
159}
160
161/// The positions of an [`AtomSelection`], ascending.
162#[derive(Clone, Debug)]
163pub enum SelectionIter<'a> {
164    /// One contiguous run, which also covers the empty and whole-structure
165    /// selections.
166    Run(Range<u32>),
167    /// Several runs, walked one after another.
168    Runs {
169        /// The remaining runs, including the one being walked.
170        runs: &'a [Range<u32>],
171        /// The index of the next run to start.
172        next: usize,
173        /// The unfinished part of the current run.
174        current: Range<u32>,
175    },
176    /// Scattered positions read straight from their slice.
177    Sparse(std::slice::Iter<'a, u32>),
178    /// Set bits of a dense mask.
179    Dense(Ones<'a>),
180}
181
182impl Iterator for SelectionIter<'_> {
183    type Item = u32;
184
185    fn next(&mut self) -> Option<u32> {
186        match self {
187            Self::Run(run) => run.next(),
188            Self::Runs {
189                runs,
190                next,
191                current,
192            } => loop {
193                if let Some(position) = current.next() {
194                    return Some(position);
195                }
196                let run = runs.get(*next)?;
197                *next = next.checked_add(1)?;
198                *current = run.start..run.end;
199            },
200            Self::Sparse(positions) => positions.next().copied(),
201            Self::Dense(ones) => ones.next(),
202        }
203    }
204
205    fn size_hint(&self) -> (usize, Option<usize>) {
206        match self {
207            Self::Run(run) => run.size_hint(),
208            Self::Runs {
209                runs,
210                next,
211                current,
212            } => {
213                let remaining = match runs.get(*next..) {
214                    Some(tail) => tail.iter().map(ExactSizeIterator::len).sum::<usize>(),
215                    None => 0,
216                };
217                let total = current.len() + remaining;
218                (total, Some(total))
219            }
220            Self::Sparse(positions) => positions.size_hint(),
221            Self::Dense(ones) => ones.size_hint(),
222        }
223    }
224}
225
226impl ExactSizeIterator for SelectionIter<'_> {}
227
228impl std::iter::FusedIterator for SelectionIter<'_> {}
229
230impl FromIterator<u32> for AtomSelection {
231    fn from_iter<T: IntoIterator<Item = u32>>(iter: T) -> Self {
232        let mut positions: Vec<u32> = iter.into_iter().collect();
233        positions.sort_unstable();
234        positions.dedup();
235        Self::from_sorted(positions)
236    }
237}
238
239impl<'a> IntoIterator for &'a AtomSelection {
240    type Item = u32;
241    type IntoIter = SelectionIter<'a>;
242
243    fn into_iter(self) -> Self::IntoIter {
244        self.iter()
245    }
246}
247
248fn inline_runs(positions: &[u32]) -> Option<SmallVec<[Range<u32>; 4]>> {
249    let mut runs = SmallVec::<[Range<u32>; 4]>::new();
250    for position in positions.iter().copied() {
251        let end = position.checked_add(1)?;
252        if let Some(run) = runs.last_mut()
253            && run.end == position
254        {
255            run.end = end;
256            continue;
257        }
258        if runs.len() == runs.inline_size() {
259            return None;
260        }
261        runs.push(position..end);
262    }
263    Some(runs)
264}
265
266fn mask_from_positions(len: u32, positions: &[u32]) -> BitVec {
267    let mut mask = BitVec::repeat(false, len);
268    for position in positions {
269        mask.set(*position, true);
270    }
271    mask
272}
273
274fn selection_len(selection: &AtomSelection) -> u64 {
275    match selection {
276        AtomSelection::Empty => 0,
277        AtomSelection::All(count) => u64::from(*count),
278        AtomSelection::Range(run) => u64::from(run.end - run.start),
279        AtomSelection::Ranges(runs) => runs.iter().map(|run| u64::from(run.end - run.start)).sum(),
280        AtomSelection::Sparse(positions) => positions.len() as u64,
281        AtomSelection::Dense(mask) => u64::from(mask.count_ones()),
282    }
283}