molframe_core/chunk/parent.rs
1//! Finding the residue an atom belongs to.
2//!
3//! The downward direction is free: a residue's atoms are a range, so it is two
4//! loads and an addition. The upward direction is the awkward one, and it admits
5//! a trade the other does not.
6//!
7//! Storing a residue number beside every atom answers it in one load and costs
8//! four bytes per atom — sixteen megabytes on a four-million-atom virus capsid,
9//! which is cache the geometric kernels do not get. Storing nothing answers it
10//! by binary search over the residue offsets, which is fine occasionally and
11//! poor in a loop.
12//!
13//! The default is neither: record the residue at the start of every block of a
14//! hundred and twenty-eight atoms and scan forward within the block. That is
15//! constant work per lookup at a thirty-second of the memory, because a block
16//! spans a handful of residues and the scan never leaves one cache line.
17
18use crate::index::ResidueIndex;
19use crate::topology::ResidueTable;
20use std::mem::size_of_val;
21
22/// How an atom's residue is recovered.
23#[derive(Clone, PartialEq, Eq, Debug, Default)]
24pub enum ParentMapping {
25 /// Nothing stored; search the residue offsets.
26 #[default]
27 OffsetsOnly,
28 /// One residue position per atom.
29 Explicit(Vec<u32>),
30 /// One residue position per block of atoms, refined by a bounded scan.
31 BlockIndexed {
32 /// Atoms per block.
33 block: u16,
34 /// The residue the first atom of each block belongs to.
35 parents: Vec<u32>,
36 },
37}
38
39impl ParentMapping {
40 /// Atoms per block in the default mapping.
41 ///
42 /// A hundred and twenty-eight four-byte entries is one entry per five
43 /// hundred and twelve bytes of atom data, and the scan it implies stays
44 /// inside a few residues.
45 pub const DEFAULT_BLOCK: u16 = 128;
46
47 /// Builds the default mapping for a chunk whose atoms start at `first_atom`.
48 ///
49 /// `parent_of` is the residue of each atom in chunk order.
50 #[must_use]
51 pub fn block_indexed(parents_in_order: &[u32]) -> Self {
52 let block = Self::DEFAULT_BLOCK;
53 let parents = parents_in_order
54 .iter()
55 .step_by(usize::from(block))
56 .copied()
57 .collect();
58
59 Self::BlockIndexed { block, parents }
60 }
61
62 /// Builds a mapping that answers in one load, for a chunk whose atoms are
63 /// looked up individually often enough to justify the memory.
64 #[must_use]
65 pub fn explicit(parents_in_order: &[u32]) -> Self {
66 Self::Explicit(parents_in_order.to_vec())
67 }
68
69 /// The bytes this mapping costs for `atoms` atoms.
70 #[must_use]
71 pub fn bytes(&self) -> usize {
72 match self {
73 Self::OffsetsOnly => 0,
74 Self::Explicit(parents) | Self::BlockIndexed { parents, .. } => {
75 size_of_val(parents.as_slice())
76 }
77 }
78 }
79
80 /// The residue of the atom at `local` within a chunk whose atoms begin at
81 /// `first_atom` in the structure's flat order.
82 ///
83 /// `residues` is consulted only where the mapping cannot answer alone.
84 #[must_use]
85 pub fn resolve(
86 &self,
87 local: u32,
88 first_atom: u32,
89 residues: &ResidueTable,
90 ) -> Option<ResidueIndex> {
91 match self {
92 Self::OffsetsOnly => Self::resolve_from_offsets(local, first_atom, residues),
93 Self::Explicit(parents) => Self::resolve_explicit(local, parents),
94 Self::BlockIndexed { block, parents } => {
95 Self::resolve_block_indexed(local, first_atom, *block, parents, residues)
96 }
97 }
98 }
99
100 fn resolve_from_offsets(
101 local: u32,
102 first_atom: u32,
103 residues: &ResidueTable,
104 ) -> Option<ResidueIndex> {
105 first_atom
106 .checked_add(local)
107 .and_then(|atom| residues.containing(atom))
108 }
109
110 fn resolve_explicit(local: u32, parents: &[u32]) -> Option<ResidueIndex> {
111 usize::try_from(local)
112 .ok()
113 .and_then(|index| parents.get(index))
114 .copied()
115 .map(ResidueIndex::new)
116 }
117
118 fn resolve_block_indexed(
119 local: u32,
120 first_atom: u32,
121 block: u16,
122 parents: &[u32],
123 residues: &ResidueTable,
124 ) -> Option<ResidueIndex> {
125 let start = Self::block_parent(local, block, parents)?;
126
127 let atom = first_atom.checked_add(local)?;
128
129 Self::scan_forward(start, atom, residues)
130 }
131
132 fn block_parent(local: u32, block: u16, parents: &[u32]) -> Option<u32> {
133 let block = u32::from(block).max(1);
134 let block_index = local / block;
135
136 usize::try_from(block_index)
137 .ok()
138 .and_then(|index| parents.get(index))
139 .copied()
140 }
141
142 /// Walks forward from a known residue until the one containing `atom`.
143 ///
144 /// Bounded by the number of residues a block spans, which is small by
145 /// construction. Returns nothing rather than looping if the tables are
146 /// inconsistent with each other.
147 fn scan_forward(start: u32, atom: u32, residues: &ResidueTable) -> Option<ResidueIndex> {
148 let start = usize::try_from(start).ok()?;
149
150 (start..residues.len())
151 .map_while(|candidate| {
152 let candidate = u32::try_from(candidate).ok()?;
153
154 let residue = ResidueIndex::new(candidate);
155 let range = residues.atoms(residue)?;
156
157 (atom >= range.start).then_some((residue, range))
158 })
159 .find_map(|(residue, range)| range.contains(&atom).then_some(residue))
160 }
161}
162
163#[cfg(test)]
164#[path = "parent_tests.rs"]
165mod tests;