Skip to main content

molframe_core/structure/
handle.rs

1//! Walking the hierarchy without building objects.
2//!
3//! A handle is a borrow of the structure plus a position — two words, `Copy`,
4//! and allocation-free. Iterating a million atoms therefore allocates nothing at
5//! all, which is the point on which an object-per-atom model becomes unusable at
6//! scale while still reading naturally.
7//!
8//! The hierarchy is a computed view over the tables, not the storage. These
9//! handles are how it is computed.
10
11use super::data::StructureData;
12use super::handle_lookup::{find_named, range_or_empty, recorded_value};
13use crate::chunk::AtomChunk;
14use crate::element::Element;
15use crate::index::{AtomIndex, ChainIndex, EntityIndex, ModelIndex, ResidueIndex};
16use crate::symbol::{AltId, SymbolId};
17use crate::topology::PolymerKind;
18
19/// One model of a structure.
20#[derive(Clone, Copy, Debug)]
21pub struct ModelRef<'a> {
22    data: &'a StructureData,
23    index: ModelIndex,
24}
25
26/// One chain.
27#[derive(Clone, Copy, Debug)]
28pub struct ChainRef<'a> {
29    pub(crate) data: &'a StructureData,
30    pub(crate) index: ChainIndex,
31}
32
33/// One residue.
34#[derive(Clone, Copy, Debug)]
35pub struct ResidueRef<'a> {
36    data: &'a StructureData,
37    index: ResidueIndex,
38}
39
40/// One atom.
41#[derive(Clone, Copy, Debug)]
42pub struct AtomRef<'a> {
43    data: &'a StructureData,
44    index: AtomIndex,
45}
46
47impl<'a> ModelRef<'a> {
48    const fn new(data: &'a StructureData, index: ModelIndex) -> Self {
49        Self { data, index }
50    }
51
52    /// This model's position.
53    #[must_use]
54    pub const fn index(self) -> ModelIndex {
55        self.index
56    }
57
58    /// The number the model was deposited under, which is never reassigned.
59    #[must_use]
60    pub fn number(self) -> Option<i32> {
61        self.data.topology.models.model_num(self.index)
62    }
63
64    /// The chains of this model.
65    pub fn chains(self) -> impl Iterator<Item = ChainRef<'a>> {
66        let data = self.data;
67        let range = range_or_empty(self.data.topology.models.chains(self.index));
68
69        range.map(move |position| ChainRef::new(data, ChainIndex::new(position)))
70    }
71
72    /// The chain with this label, in either namespace.
73    #[must_use]
74    pub fn chain(self, label: &str) -> Option<ChainRef<'a>> {
75        let wanted = self.data.dictionary.get(label)?;
76
77        self.chains().find(|chain| chain.has_label(wanted))
78    }
79
80    /// The chain at one position within this model.
81    #[must_use]
82    pub fn chain_at(self, position: usize) -> Option<ChainRef<'a>> {
83        self.chains().nth(position)
84    }
85}
86
87impl<'a> ChainRef<'a> {
88    const fn new(data: &'a StructureData, index: ChainIndex) -> Self {
89        Self { data, index }
90    }
91
92    /// This chain's position.
93    #[must_use]
94    pub const fn index(self) -> ChainIndex {
95        self.index
96    }
97
98    /// The normalised chain label.
99    #[must_use]
100    pub fn label_asym_id(self) -> Option<SymbolId> {
101        self.data.topology.chains.label_asym_id(self.index)
102    }
103
104    /// The normalised chain label as text.
105    #[must_use]
106    pub fn label(self) -> Option<&'a str> {
107        self.data.dictionary.resolve(self.label_asym_id()?)
108    }
109
110    /// The depositor's chain label, where the file carried one.
111    #[must_use]
112    pub fn auth_asym_id(self) -> Option<SymbolId> {
113        self.data.topology.chains.auth_asym_id(self.index)
114    }
115
116    /// The depositor's chain label as text.
117    #[must_use]
118    pub fn auth_label(self) -> Option<&'a str> {
119        self.data.dictionary.resolve(self.auth_asym_id()?)
120    }
121
122    /// The species this chain instantiates.
123    #[must_use]
124    pub fn entity(self) -> Option<EntityIndex> {
125        self.data.topology.chains.entity(self.index)
126    }
127
128    /// What kind of polymer this chain is.
129    #[must_use]
130    pub fn polymer_kind(self) -> PolymerKind {
131        match self.data.topology.chains.polymer_kind(self.index) {
132            Some(kind) => kind,
133            None => PolymerKind::None,
134        }
135    }
136
137    /// The residues of this chain.
138    pub fn residues(self) -> impl Iterator<Item = ResidueRef<'a>> {
139        let data = self.data;
140        let range = range_or_empty(self.data.topology.chains.residues(self.index));
141
142        range.map(move |position| ResidueRef::new(data, ResidueIndex::new(position)))
143    }
144
145    /// The residue with this number, in whichever namespace carries it.
146    #[must_use]
147    pub fn residue(self, number: i32) -> Option<ResidueRef<'a>> {
148        self.residues().find(|residue| residue.has_number(number))
149    }
150
151    fn has_label(self, wanted: SymbolId) -> bool {
152        self.label_asym_id() == Some(wanted) || self.auth_asym_id() == Some(wanted)
153    }
154
155    /// The residue at one position within this chain.
156    #[must_use]
157    pub fn residue_at(self, position: usize) -> Option<ResidueRef<'a>> {
158        self.residues().nth(position)
159    }
160}
161
162impl<'a> ResidueRef<'a> {
163    const fn new(data: &'a StructureData, index: ResidueIndex) -> Self {
164        Self { data, index }
165    }
166
167    /// This residue's position.
168    #[must_use]
169    pub const fn index(self) -> ResidueIndex {
170        self.index
171    }
172
173    /// The normalised component code.
174    #[must_use]
175    pub fn label_comp_id(self) -> Option<SymbolId> {
176        self.data.topology.residues.label_comp_id(self.index)
177    }
178
179    /// The component code as a string.
180    #[must_use]
181    pub fn name(self) -> Option<&'a str> {
182        self.label_comp_id()
183            .and_then(|symbol| self.data.dictionary.resolve(symbol))
184    }
185
186    /// The depositor's component code, where it differs or was recorded.
187    #[must_use]
188    pub fn auth_comp_id(self) -> Option<SymbolId> {
189        self.data.topology.residues.auth_comp_id(self.index)
190    }
191
192    /// The depositor's component code as text.
193    #[must_use]
194    pub fn auth_name(self) -> Option<&'a str> {
195        self.data.dictionary.resolve(self.auth_comp_id()?)
196    }
197
198    /// The sequence position within the entity, absent for a non-polymer.
199    #[must_use]
200    pub fn label_seq_id(self) -> Option<i32> {
201        self.data.topology.residues.label_seq_id(self.index)
202    }
203
204    /// The depositor's residue number.
205    #[must_use]
206    pub fn auth_seq_id(self) -> Option<i32> {
207        self.data.topology.residues.auth_seq_id(self.index)
208    }
209
210    /// The insertion code, which is part of this residue's identity.
211    #[must_use]
212    pub fn ins_code(self) -> Option<&'a str> {
213        self.data
214            .topology
215            .residues
216            .ins_code(self.index)
217            .and_then(|symbol| self.data.dictionary.resolve(symbol))
218    }
219
220    /// Whether the file recorded this residue as a heterogen.
221    #[must_use]
222    pub fn is_het(self) -> bool {
223        self.data.topology.residues.is_het(self.index)
224    }
225
226    /// The atoms of this residue.
227    pub fn atoms(self) -> impl Iterator<Item = AtomRef<'a>> {
228        let data = self.data;
229        let range = range_or_empty(self.data.topology.residues.atoms(self.index));
230
231        range.map(move |position| AtomRef::new(data, AtomIndex::new(position)))
232    }
233
234    /// The atom with this name.
235    ///
236    /// Resolves the name once and then reads names straight from the chunks
237    /// that hold this residue's atoms, so the lookup costs the residue rather
238    /// than the structure. A residue's atoms are contiguous and never split
239    /// across chunks, which is what lets the walk hold still.
240    #[must_use]
241    pub fn atom(self, name: &str) -> Option<AtomRef<'a>> {
242        let wanted = self.data.dictionary.get(name)?;
243        let range = range_or_empty(self.data.topology.residues.atoms(self.index));
244        let position = find_named(&self.data.chunks, range, wanted)?;
245
246        Some(AtomRef::new(self.data, AtomIndex::new(position)))
247    }
248
249    fn has_number(self, number: i32) -> bool {
250        self.auth_seq_id() == Some(number) || self.label_seq_id() == Some(number)
251    }
252
253    /// The atom at one position within this residue.
254    #[must_use]
255    pub fn atom_at(self, position: usize) -> Option<AtomRef<'a>> {
256        self.atoms().nth(position)
257    }
258}
259
260impl<'a> AtomRef<'a> {
261    const fn new(data: &'a StructureData, index: AtomIndex) -> Self {
262        Self { data, index }
263    }
264
265    /// This atom's position in the flat atom order.
266    #[must_use]
267    pub const fn index(self) -> AtomIndex {
268        self.index
269    }
270
271    /// The chunk holding this atom, and the position within it.
272    ///
273    /// Chunks tile the atom order in ascending ranges, so this is a binary
274    /// search rather than a scan. It still runs once per attribute read, which
275    /// is why a kernel walks chunks directly and leaves handles to the code
276    /// that reads a few atoms rather than all of them.
277    fn located(self) -> Option<(&'a AtomChunk, u32)> {
278        let position = self.index.get();
279        let candidate = self
280            .data
281            .chunks
282            .partition_point(|chunk| chunk.atoms().end <= position);
283
284        let chunk = self.data.chunks.get(candidate)?;
285        let atoms = chunk.atoms();
286
287        if !atoms.contains(&position) {
288            return None;
289        }
290
291        Some((chunk, position - atoms.start))
292    }
293
294    /// The interned atom name.
295    #[must_use]
296    pub fn name_symbol(self) -> Option<SymbolId> {
297        let (chunk, local) = self.located()?;
298        chunk.atom_name(local)
299    }
300
301    /// The atom name as a string.
302    #[must_use]
303    pub fn name(self) -> Option<&'a str> {
304        self.name_symbol()
305            .and_then(|symbol| self.data.dictionary.resolve(symbol))
306    }
307
308    /// The depositor's atom name, where it differs or was recorded.
309    #[must_use]
310    pub fn auth_name_symbol(self) -> Option<SymbolId> {
311        let (chunk, local) = self.located()?;
312        chunk.auth_atom_name(local)
313    }
314
315    /// The depositor's atom name as text.
316    #[must_use]
317    pub fn auth_name(self) -> Option<&'a str> {
318        self.data.dictionary.resolve(self.auth_name_symbol()?)
319    }
320
321    /// The element.
322    #[must_use]
323    pub fn element(self) -> Option<Element> {
324        let (chunk, local) = self.located()?;
325        chunk.element(local)
326    }
327
328    /// The alternate-location label.
329    #[must_use]
330    pub fn alt_id(self) -> Option<AltId> {
331        let (chunk, local) = self.located()?;
332        chunk.alt_id(local)
333    }
334
335    /// The alternate-location label as text, absent for the shared blank form.
336    #[must_use]
337    pub fn alt_label(self) -> Option<&'a str> {
338        self.data.dictionary.resolve(self.alt_id()?.symbol()?)
339    }
340
341    /// The component identity this atom carries.
342    ///
343    /// Most atoms inherit the residue's primary component. An alternate
344    /// conformation may carry a different identity, which takes precedence.
345    #[must_use]
346    pub fn component_id(self) -> Option<SymbolId> {
347        let (chunk, local) = self.located()?;
348        if let Some(component) = chunk.alternate_component_id(local) {
349            return Some(component);
350        }
351        self.residue()?.label_comp_id()
352    }
353
354    /// The atom's effective component identity as text.
355    #[must_use]
356    pub fn component_name(self) -> Option<&'a str> {
357        self.data.dictionary.resolve(self.component_id()?)
358    }
359
360    /// The temperature factor, where the file recorded one.
361    #[must_use]
362    pub fn b_factor(self) -> Option<f32> {
363        let (chunk, local) = self.located()?;
364
365        chunk.b_factor(local).and_then(recorded_value)
366    }
367
368    /// The occupancy, where the file recorded one.
369    #[must_use]
370    pub fn occupancy(self) -> Option<f32> {
371        let (chunk, local) = self.located()?;
372
373        chunk.occupancy(local).and_then(recorded_value)
374    }
375
376    /// The formal charge, where the file recorded one.
377    #[must_use]
378    pub fn formal_charge(self) -> Option<i8> {
379        let (chunk, local) = self.located()?;
380        let (value, presence) = chunk.formal_charge(local)?;
381        presence.is_present().then_some(value)
382    }
383
384    /// The file-local atom-site identifier.
385    #[must_use]
386    pub fn atom_site_id(self) -> Option<u32> {
387        let (chunk, local) = self.located()?;
388        chunk.atom_site_id(local)
389    }
390
391    /// The position in the first model, where one was recorded.
392    #[must_use]
393    pub fn position(self) -> Option<[f32; 3]> {
394        let (chunk, local) = self.located()?;
395
396        if !chunk.has_position(local) {
397            return None;
398        }
399
400        self.data
401            .coords
402            .block(ModelIndex::new(0))?
403            .as_slice()
404            .get(self.index.as_usize())
405            .copied()
406    }
407
408    /// The residue this atom belongs to.
409    #[must_use]
410    pub fn residue(self) -> Option<ResidueRef<'a>> {
411        let (chunk, local) = self.located()?;
412        let index = chunk.residue(local, &self.data.topology.residues)?;
413
414        Some(ResidueRef::new(self.data, index))
415    }
416}
417
418impl StructureData {
419    /// A handle on one model.
420    #[must_use]
421    pub fn model(&self, model: ModelIndex) -> Option<ModelRef<'_>> {
422        (model.as_usize() < self.topology.models.len()).then_some(ModelRef::new(self, model))
423    }
424
425    /// Every model.
426    pub fn models(&self) -> impl Iterator<Item = ModelRef<'_>> {
427        self.topology
428            .models
429            .iter()
430            .map(move |index| ModelRef::new(self, index))
431    }
432
433    /// A handle on one chain.
434    #[must_use]
435    pub fn chain(&self, chain: ChainIndex) -> Option<ChainRef<'_>> {
436        (chain.as_usize() < self.topology.chains.len()).then_some(ChainRef {
437            data: self,
438            index: chain,
439        })
440    }
441
442    /// A handle on one residue.
443    #[must_use]
444    pub fn residue(&self, residue: ResidueIndex) -> Option<ResidueRef<'_>> {
445        (residue.as_usize() < self.topology.residues.len()).then_some(ResidueRef {
446            data: self,
447            index: residue,
448        })
449    }
450
451    /// Every chain, across every model.
452    pub fn chains(&self) -> impl Iterator<Item = ChainRef<'_>> {
453        self.topology
454            .chains
455            .iter()
456            .map(move |index| ChainRef::new(self, index))
457    }
458
459    /// The chain with this label, in either namespace.
460    #[must_use]
461    pub fn chain_named(&self, label: &str) -> Option<ChainRef<'_>> {
462        let wanted = self.dictionary.get(label)?;
463        self.chains().find(|chain| {
464            chain.label_asym_id() == Some(wanted) || chain.auth_asym_id() == Some(wanted)
465        })
466    }
467
468    /// Every residue, across every chain.
469    pub fn residues(&self) -> impl Iterator<Item = ResidueRef<'_>> {
470        (0..self.topology.residues.len()).filter_map(move |position| {
471            u32::try_from(position)
472                .ok()
473                .map(|position| ResidueRef::new(self, ResidueIndex::new(position)))
474        })
475    }
476
477    /// Every atom, in file order.
478    pub fn atoms(&self) -> impl Iterator<Item = AtomRef<'_>> {
479        (0..self.atom_count()).map(move |position| AtomRef::new(self, AtomIndex::new(position)))
480    }
481
482    /// A handle on one atom.
483    #[must_use]
484    pub fn atom(&self, atom: AtomIndex) -> Option<AtomRef<'_>> {
485        let count = self.chunks.last().map_or(0, |chunk| chunk.atoms().end);
486        (atom.get() < count).then_some(AtomRef {
487            data: self,
488            index: atom,
489        })
490    }
491}
492
493#[cfg(test)]
494#[path = "handle_tests.rs"]
495mod tests;