Skip to main content

molframe_core/chunk/
atom.rs

1//! A contiguous run of atoms, stored column by column.
2//!
3//! Positions are not here. They live in one contiguous buffer per frame and a
4//! chunk names the range of it that belongs to the chunk, so the whole
5//! coordinate set can be handed out as a pointer while a kernel still reads a
6//! chunk's worth at a time.
7
8use super::parent::ParentMapping;
9use super::stats::AtomChunkStats;
10use crate::column::{EncodedColumn, Presence, ValidityMask};
11use crate::coords::CoordinateBlock;
12use crate::element::Element;
13use crate::index::ResidueIndex;
14use crate::optional::OptionalSymbol;
15use crate::symbol::{AltId, SymbolId};
16use crate::topology::ResidueTable;
17use std::ops::Range;
18
19/// One atom's worth of everything a chunk stores.
20#[derive(Clone, Copy, Debug)]
21pub struct AtomRecord {
22    /// The atom's position, or `None` when the file did not record one.
23    pub position: Option<[f32; 3]>,
24    /// The element.
25    pub element: Element,
26    /// The normalised atom name.
27    pub atom_name: SymbolId,
28    /// The depositor's atom name, where it differs or where the file carried it.
29    pub auth_atom_name: OptionalSymbol,
30    /// A component identity differing from the residue's primary identity.
31    ///
32    /// This occurs when alternate conformations model different residue types.
33    /// Absence means the residue table supplies the component identity.
34    pub alternate_component_id: OptionalSymbol,
35    /// The alternate-location label.
36    pub alt_id: AltId,
37    /// The residue this atom belongs to.
38    pub residue: ResidueIndex,
39    /// Occupancy, and whether it was recorded.
40    pub occupancy: (f32, Presence),
41    /// Temperature factor, and whether it was recorded.
42    pub b_factor: (f32, Presence),
43    /// Formal charge, and whether it was recorded.
44    pub formal_charge: (i8, Presence),
45    /// The file-local serial number, retained because other records cite it.
46    pub atom_site_id: u32,
47}
48
49/// A contiguous run of atoms, stored column by column.
50#[derive(Clone, Debug)]
51pub struct AtomChunk {
52    pub(super) atoms: Range<u32>,
53    pub(super) model: u32,
54    pub(super) element: EncodedColumn<u8>,
55    pub(super) atom_name: EncodedColumn<SymbolId>,
56    pub(super) auth_atom_name: Option<EncodedColumn<u32>>,
57    pub(super) alternate_component_id: Option<EncodedColumn<u32>>,
58    pub(super) alt_id: EncodedColumn<u32>,
59    pub(super) residue: ParentMapping,
60    pub(super) occupancy: EncodedColumn<f32>,
61    pub(super) occupancy_validity: ValidityMask,
62    pub(super) b_factor: EncodedColumn<f32>,
63    pub(super) b_factor_validity: ValidityMask,
64    pub(super) formal_charge: EncodedColumn<i8>,
65    pub(super) formal_charge_validity: ValidityMask,
66    pub(super) atom_site_id: EncodedColumn<u32>,
67    pub(super) coord_validity: ValidityMask,
68    pub(super) stats: AtomChunkStats,
69}
70
71impl AtomChunk {
72    /// The number of atoms.
73    ///
74    #[must_use]
75    pub fn len(&self) -> u32 {
76        self.atoms.end - self.atoms.start
77    }
78
79    /// Returns true when the chunk holds no atoms.
80    #[must_use]
81    pub fn is_empty(&self) -> bool {
82        self.len() == 0
83    }
84
85    /// The atoms this chunk covers, in the structure's flat order.
86    ///
87    /// This is also the range of the coordinate buffer that belongs to it.
88    #[must_use]
89    pub fn atoms(&self) -> Range<u32> {
90        self.atom_range()
91    }
92
93    /// What the chunk can say about itself without being read.
94    #[must_use]
95    pub const fn stats(&self) -> &AtomChunkStats {
96        &self.stats
97    }
98
99    /// The model every atom in this chunk belongs to.
100    #[must_use]
101    pub const fn model(&self) -> u32 {
102        self.model
103    }
104
105    /// The positions of this chunk's atoms, borrowed from the frame's buffer.
106    #[must_use]
107    pub fn positions<'a>(&self, coords: &'a CoordinateBlock) -> Option<&'a [[f32; 3]]> {
108        coords.range(self.atom_range())
109    }
110
111    /// The element of the atom at `local`.
112    #[must_use]
113    pub fn element(&self, local: u32) -> Option<Element> {
114        self.element.get(local).map(Element::from_atomic_number)
115    }
116
117    /// The normalised name of the atom at `local`.
118    #[must_use]
119    pub fn atom_name(&self, local: u32) -> Option<SymbolId> {
120        self.atom_name.get(local)
121    }
122
123    /// Plain backing storage for atom names, when no decode is required.
124    #[must_use]
125    pub fn atom_names_plain(&self) -> Option<&[SymbolId]> {
126        self.atom_name.as_slice()
127    }
128
129    /// The depositor's name for the atom at `local`, where the file carried one.
130    #[must_use]
131    pub fn auth_atom_name(&self, local: u32) -> Option<SymbolId> {
132        optional_symbol(self.auth_atom_name.as_ref()?.get(local)?)
133    }
134
135    /// The atom-specific component identity, where it differs from its residue.
136    #[must_use]
137    pub fn alternate_component_id(&self, local: u32) -> Option<SymbolId> {
138        optional_symbol(self.alternate_component_id.as_ref()?.get(local)?)
139    }
140
141    /// The alternate-location label of the atom at `local`.
142    #[must_use]
143    pub fn alt_id(&self, local: u32) -> Option<AltId> {
144        self.alt_id.get(local).map(AltId::from_raw)
145    }
146
147    /// The residue of the atom at `local`.
148    #[must_use]
149    pub fn residue(&self, local: u32, residues: &ResidueTable) -> Option<ResidueIndex> {
150        self.residue.resolve(local, self.atoms.start, residues)
151    }
152
153    /// The occupancy of the atom at `local`, and whether it was recorded.
154    #[must_use]
155    pub fn occupancy(&self, local: u32) -> Option<(f32, Presence)> {
156        with_presence(self.occupancy.get(local), &self.occupancy_validity, local)
157    }
158
159    /// Which occupancies were recorded.
160    #[must_use]
161    pub const fn occupancy_validity(&self) -> &ValidityMask {
162        &self.occupancy_validity
163    }
164
165    /// Plain backing storage for occupancies, when no decode is required.
166    #[must_use]
167    pub fn occupancies_plain(&self) -> Option<&[f32]> {
168        self.occupancy.as_slice()
169    }
170
171    /// The temperature factor of the atom at `local`, and whether it was recorded.
172    #[must_use]
173    pub fn b_factor(&self, local: u32) -> Option<(f32, Presence)> {
174        with_presence(self.b_factor.get(local), &self.b_factor_validity, local)
175    }
176
177    /// Which temperature factors were recorded.
178    #[must_use]
179    pub const fn b_factor_validity(&self) -> &ValidityMask {
180        &self.b_factor_validity
181    }
182
183    /// Plain backing storage for temperature factors, when no decode is required.
184    #[must_use]
185    pub fn b_factors_plain(&self) -> Option<&[f32]> {
186        self.b_factor.as_slice()
187    }
188
189    /// The formal charge of the atom at `local`, and whether it was recorded.
190    #[must_use]
191    pub fn formal_charge(&self, local: u32) -> Option<(i8, Presence)> {
192        with_presence(
193            self.formal_charge.get(local),
194            &self.formal_charge_validity,
195            local,
196        )
197    }
198
199    /// The file-local serial number of the atom at `local`.
200    #[must_use]
201    pub fn atom_site_id(&self, local: u32) -> Option<u32> {
202        self.atom_site_id.get(local)
203    }
204
205    /// Whether the position of the atom at `local` was recorded.
206    #[must_use]
207    pub fn has_position(&self, local: u32) -> bool {
208        self.coord_validity.get(local).is_present()
209    }
210
211    /// Reconstructs one logical row without changing its encoded storage.
212    ///
213    /// Editors use this only when a structural change requires rebuilding atom
214    /// ordinals. Ordinary reads stay columnar and never materialise records.
215    #[must_use]
216    pub fn record(
217        &self,
218        local: u32,
219        residues: &ResidueTable,
220        position: Option<[f32; 3]>,
221    ) -> Option<AtomRecord> {
222        let position = if self.has_position(local) {
223            Some(position?)
224        } else {
225            None
226        };
227        Some(AtomRecord {
228            position,
229            element: self.element(local)?,
230            atom_name: self.atom_name(local)?,
231            auth_atom_name: optional(self.auth_atom_name(local)),
232            alternate_component_id: optional(self.alternate_component_id(local)),
233            alt_id: self.alt_id(local)?,
234            residue: self.residue(local, residues)?,
235            occupancy: self.occupancy(local)?,
236            b_factor: self.b_factor(local)?,
237            formal_charge: self.formal_charge(local)?,
238            atom_site_id: self.atom_site_id(local)?,
239        })
240    }
241
242    fn atom_range(&self) -> Range<u32> {
243        self.atoms.start..self.atoms.end
244    }
245}
246
247fn with_presence<T>(
248    value: Option<T>,
249    validity: &ValidityMask,
250    local: u32,
251) -> Option<(T, Presence)> {
252    value.map(|value| (value, validity.get(local)))
253}
254
255fn optional_symbol(raw: u32) -> Option<SymbolId> {
256    (raw != u32::MAX).then(|| SymbolId::from_raw(raw))
257}
258
259fn optional(symbol: Option<SymbolId>) -> OptionalSymbol {
260    match symbol {
261        Some(symbol) => OptionalSymbol::some(symbol),
262        None => OptionalSymbol::NONE,
263    }
264}