Skip to main content

molframe_core/column/
validity.rs

1//! Why a value is not there.
2//!
3//! `Option` collapses three different statements into one. A file can say a
4//! field does not apply here, that its value exists but was not recorded, or
5//! simply give a value — and those mean different things to an analysis. A
6//! B-factor that does not apply is not a B-factor that nobody measured.
7//!
8//! A fourth case is deliberately *not* represented here: an atom that was never
9//! modelled. That atom has no row at all. Inventing a row with an absent
10//! position would fabricate an atom the experiment never saw, and the gap
11//! between what a component should contain and what the model contains is
12//! reported as coverage instead.
13
14use super::bits::BitVec;
15
16/// What a column says about one position.
17#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
18pub enum Presence {
19    /// A value is recorded.
20    #[default]
21    Present,
22    /// The value exists but was not recorded.
23    Unknown,
24    /// The field does not apply here.
25    Inapplicable,
26}
27
28impl Presence {
29    /// Returns true when a value is recorded.
30    #[must_use]
31    pub const fn is_present(self) -> bool {
32        matches!(self, Self::Present)
33    }
34
35    const fn from_flags(present: bool, unknown: bool) -> Self {
36        match (present, unknown) {
37            (true, _) => Self::Present,
38            (false, true) => Self::Unknown,
39            (false, false) => Self::Inapplicable,
40        }
41    }
42}
43
44/// Which positions of a column carry a value, and why the others do not.
45///
46/// The overwhelmingly common case is that every position carries one, and that
47/// case costs a length rather than two bit sets.
48///
49/// # Examples
50///
51/// ```
52/// use molframe_core::column::{Presence, ValidityMask};
53///
54/// let mut mask = ValidityMask::all_present(3);
55/// assert_eq!(mask.get(0), Presence::Present);
56///
57/// mask.set(1, Presence::Unknown);
58/// assert_eq!(mask.get(1), Presence::Unknown);
59/// assert_eq!(mask.present_count(), 2);
60/// ```
61#[derive(Clone, PartialEq, Eq, Debug)]
62pub enum ValidityMask {
63    /// Every position carries a value.
64    AllPresent(u32),
65    /// Positions differ, so both distinctions are tracked.
66    Mixed {
67        /// Set where a value is recorded.
68        present: BitVec,
69        /// Set where the value exists but was not recorded. Meaningful only
70        /// where `present` is clear; clear there means the field does not apply.
71        unknown: BitVec,
72    },
73}
74
75impl ValidityMask {
76    /// Creates a mask over `len` positions that all carry a value.
77    #[must_use]
78    pub const fn all_present(len: u32) -> Self {
79        Self::AllPresent(len)
80    }
81
82    /// The number of positions covered.
83    #[must_use]
84    pub fn len(&self) -> u32 {
85        match self {
86            Self::AllPresent(len) => *len,
87            Self::Mixed { present, .. } => present.len(),
88        }
89    }
90
91    /// Returns true when no position is covered.
92    #[must_use]
93    pub fn is_empty(&self) -> bool {
94        self.len() == 0
95    }
96
97    /// Returns true when every covered position carries a value.
98    #[must_use]
99    pub fn is_all_present(&self) -> bool {
100        match self {
101            Self::AllPresent(_) => true,
102            Self::Mixed { present, .. } => present.all(),
103        }
104    }
105
106    /// What the column says about `position`.
107    ///
108    /// A position past the end reads as inapplicable, because a column that does
109    /// not reach that far makes no statement about it.
110    #[must_use]
111    pub fn get(&self, position: u32) -> Presence {
112        match self {
113            Self::AllPresent(len) => {
114                if position < *len {
115                    Presence::Present
116                } else {
117                    Presence::Inapplicable
118                }
119            }
120            Self::Mixed { present, unknown } => {
121                Presence::from_flags(present.test(position), unknown.test(position))
122            }
123        }
124    }
125
126    /// Records what the column says about `position`.
127    ///
128    /// Setting anything other than present on an all-present mask materialises
129    /// the bit sets, which is the only point at which they are allocated.
130    pub fn set(&mut self, position: u32, presence: Presence) {
131        if !self.materialise_for(position, presence) {
132            return;
133        }
134
135        if let Self::Mixed { present, unknown } = self {
136            present.set(position, presence == Presence::Present);
137            unknown.set(position, presence == Presence::Unknown);
138        }
139    }
140
141    fn materialise_for(&mut self, position: u32, presence: Presence) -> bool {
142        let Self::AllPresent(len) = self else {
143            return true;
144        };
145
146        if presence.is_present() || position >= *len {
147            return false;
148        }
149
150        let len = *len;
151
152        *self = Self::Mixed {
153            present: BitVec::repeat(true, len),
154            unknown: BitVec::repeat(false, len),
155        };
156
157        true
158    }
159
160    /// The number of positions carrying a value.
161    #[must_use]
162    pub fn present_count(&self) -> u32 {
163        match self {
164            Self::AllPresent(len) => *len,
165            Self::Mixed { present, .. } => present.count_ones(),
166        }
167    }
168
169    /// Collapses back to the compact form when every position carries a value.
170    ///
171    /// Worth calling once after building a column, so a file that happened to
172    /// record everything does not carry two bit sets for the rest of its life.
173    pub fn compact(&mut self) {
174        let compact_len = match self {
175            Self::Mixed { present, .. } if present.all() => Some(present.len()),
176            _ => None,
177        };
178
179        if let Some(len) = compact_len {
180            *self = Self::AllPresent(len);
181        }
182    }
183}
184
185impl ValidityMask {
186    /// Builds a validity mask, returning `None` if `u32` row capacity is exceeded.
187    pub fn try_from_iter<T: IntoIterator<Item = Presence>>(iter: T) -> Option<Self> {
188        let mut len = 0u32;
189        let mut mixed: Option<(BitVec, BitVec)> = None;
190
191        for presence in iter {
192            let next_len = len.checked_add(1)?;
193            match mixed.as_mut() {
194                Some((present, unknown)) => {
195                    push_presence(present, unknown, presence)?;
196                }
197                None if presence.is_present() => {}
198                None => {
199                    let mut present = BitVec::repeat(true, len);
200                    let mut unknown = BitVec::repeat(false, len);
201
202                    push_presence(&mut present, &mut unknown, presence)?;
203
204                    mixed = Some((present, unknown));
205                }
206            }
207
208            len = next_len;
209        }
210
211        match mixed {
212            Some((present, unknown)) => Some(Self::Mixed { present, unknown }),
213            None => Some(Self::AllPresent(len)),
214        }
215    }
216
217    pub(crate) fn from_bounded_iter<T: IntoIterator<Item = Presence>>(iter: T, len: u32) -> Self {
218        let mut present = BitVec::repeat(false, len);
219        let mut unknown = BitVec::repeat(false, len);
220        for (position, presence) in (0..len).zip(iter) {
221            present.set(position, presence == Presence::Present);
222            unknown.set(position, presence == Presence::Unknown);
223        }
224        let mut validity = Self::Mixed { present, unknown };
225        validity.compact();
226        validity
227    }
228}
229
230fn push_presence(present: &mut BitVec, unknown: &mut BitVec, presence: Presence) -> Option<()> {
231    present.try_push(presence == Presence::Present).ok()?;
232    unknown.try_push(presence == Presence::Unknown).ok()
233}
234
235#[cfg(test)]
236#[path = "validity_tests.rs"]
237mod tests;