molframe_core/chunk/stats.rs
1//! What a chunk can say about itself without being read.
2//!
3//! Every chunk carries a small summary, and the summary is the single largest
4//! source of speed in the whole design. Asked for zinc atoms within five
5//! ångström of one chain, the engine reads no rows at all from a chunk whose
6//! element set lacks zinc, and none from a chunk whose bounding box cannot come
7//! that close. On an ordinary entry that removes well over nine rows in ten
8//! before the first comparison.
9//!
10//! The summary is built once, when the chunk is, and is a few dozen bytes
11//! against a quarter of a megabyte of atom data.
12
13use crate::coords::Aabb;
14use crate::element::Element;
15
16/// The set of elements present, one bit per atomic number.
17///
18/// The periodic table fits in two machine words, so membership is a shift and a
19/// test rather than a search.
20#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
21#[repr(transparent)]
22pub struct ElementMask(u128);
23
24impl ElementMask {
25 /// The empty set.
26 pub const EMPTY: Self = Self(0);
27
28 /// Adds an element.
29 pub const fn insert(&mut self, element: Element) {
30 self.0 |= 1u128 << element.atomic_number();
31 }
32
33 /// Returns true when the element may be present.
34 #[must_use]
35 pub const fn contains(self, element: Element) -> bool {
36 self.0 >> element.atomic_number() & 1 == 1
37 }
38
39 /// Returns true when the set is empty.
40 #[must_use]
41 pub const fn is_empty(self) -> bool {
42 self.0 == 0
43 }
44
45 /// Adds everything in `other`.
46 pub const fn union_with(&mut self, other: Self) {
47 self.0 |= other.0;
48 }
49
50 /// Returns true when the two sets share an element.
51 #[must_use]
52 pub const fn intersects(self, other: Self) -> bool {
53 self.0 & other.0 != 0
54 }
55
56 /// The number of distinct elements.
57 #[must_use]
58 pub const fn len(self) -> u32 {
59 self.0.count_ones()
60 }
61}
62
63impl FromIterator<Element> for ElementMask {
64 fn from_iter<T: IntoIterator<Item = Element>>(iter: T) -> Self {
65 let mut mask = Self::EMPTY;
66 for element in iter {
67 mask.insert(element);
68 }
69 mask
70 }
71}
72
73/// A running minimum and maximum over values that may not all be recorded.
74#[derive(Clone, Copy, PartialEq, Debug, Default)]
75pub struct Extremes {
76 min: Option<f32>,
77 max: Option<f32>,
78}
79
80impl Extremes {
81 /// Records a value, ignoring one that is not a number.
82 pub fn observe(&mut self, value: f32) {
83 if !value.is_finite() {
84 return;
85 }
86 self.min = Some(match self.min {
87 Some(current) => current.min(value),
88 None => value,
89 });
90 self.max = Some(match self.max {
91 Some(current) => current.max(value),
92 None => value,
93 });
94 }
95
96 /// The smallest value recorded.
97 #[must_use]
98 pub const fn min(self) -> Option<f32> {
99 self.min
100 }
101
102 /// The largest value recorded.
103 #[must_use]
104 pub const fn max(self) -> Option<f32> {
105 self.max
106 }
107
108 /// Returns true when no value in the range can satisfy `at_least`.
109 ///
110 /// This is what lets a filter on temperature factor skip a whole chunk.
111 #[must_use]
112 pub fn excludes_above(self, at_least: f32) -> bool {
113 self.max.is_some_and(|max| max < at_least)
114 }
115
116 /// Returns true when no value in the range can satisfy `at_most`.
117 #[must_use]
118 pub fn excludes_below(self, at_most: f32) -> bool {
119 self.min.is_some_and(|min| min > at_most)
120 }
121}
122
123/// Everything a chunk knows about itself without reading a row.
124#[derive(Clone, Copy, PartialEq, Debug, Default)]
125pub struct AtomChunkStats {
126 /// The box enclosing every recorded position.
127 pub bounds: Aabb,
128 /// The elements present.
129 pub elements: ElementMask,
130 /// The model every atom belongs to. Constant within a chunk by construction.
131 pub model: u32,
132 /// The lowest residue any atom belongs to.
133 pub residue_min: u32,
134 /// The highest residue any atom belongs to.
135 pub residue_max: u32,
136 /// The range of recorded temperature factors.
137 pub b_factor: Extremes,
138 /// The range of recorded occupancies.
139 pub occupancy: Extremes,
140 /// Whether any atom carries an alternate-location label.
141 pub has_altloc: bool,
142 /// Whether any atom's position was not recorded.
143 pub has_missing_coords: bool,
144 /// Whether any atom is hydrogen.
145 pub has_hydrogen: bool,
146}
147
148impl AtomChunkStats {
149 /// Returns true when no atom in the chunk can be `element`.
150 #[must_use]
151 pub const fn excludes_element(&self, element: Element) -> bool {
152 !self.elements.contains(element)
153 }
154
155 /// Returns true when no atom in the chunk can be within `distance` of `region`.
156 #[must_use]
157 pub fn excludes_region(&self, region: &Aabb, distance: f32) -> bool {
158 !self.bounds.within(region, distance)
159 }
160
161 /// Returns true when no atom in the chunk belongs to a residue in `range`.
162 #[must_use]
163 pub const fn excludes_residues(&self, first: u32, last: u32) -> bool {
164 self.residue_max < first || self.residue_min > last
165 }
166}
167
168#[cfg(test)]
169#[path = "stats_tests.rs"]
170mod tests;