Skip to main content

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;