Skip to main content

molframe_core/structure/
data.rs

1//! The normalised structure: what a file means, rather than how it was written.
2//!
3//! A structure is immutable and shared by reference. Editing produces a new one
4//! rather than changing this one in place, which is what makes handing out a
5//! pointer to the coordinates safe: a buffer given to a caller belongs to a
6//! snapshot that can never change underneath it, and every derived index stays
7//! valid for as long as the snapshot it was built against.
8//!
9//! Formatting is not preserved here. Reproducing whitespace, column widths and
10//! quoting is the document's job; this layer preserves meaning.
11
12use super::ExtensionStore;
13use crate::annotation::AtomAnnotations;
14use crate::chunk::AtomChunk;
15use crate::coords::{CoordinateBlock, CoordinateGeneration};
16use crate::index::ModelIndex;
17use crate::symbol::{Interner, SymbolId};
18use crate::topology::Topology;
19use std::fmt;
20use std::sync::Arc;
21
22const PLACEHOLDER_TOLERANCE: f64 = 1e-6;
23
24/// What the entry as a whole says about itself.
25///
26/// The identifier is a variable-length string. Four characters was a property of
27/// one file format, not of the archive, and extended identifiers are already
28/// being issued.
29#[derive(Clone, Debug, Default)]
30pub struct EntryMetadata {
31    /// The entry identifier, as deposited.
32    pub id: Option<Box<str>>,
33    /// The entry title.
34    pub title: Option<Box<str>>,
35    /// The experimental method.
36    pub method: Option<Box<str>>,
37    /// Resolution in ångström, where the method reports one.
38    pub resolution: Option<f32>,
39}
40
41/// The crystallographic cell.
42#[derive(Clone, Copy, PartialEq, Debug)]
43pub struct UnitCell {
44    /// Cell edge lengths in ångström.
45    pub lengths: [f64; 3],
46    /// Cell angles in degrees.
47    pub angles: [f64; 3],
48}
49
50impl UnitCell {
51    /// Returns true when the cell is the placeholder some files write when they
52    /// have no cell at all.
53    ///
54    /// A unit cube with right angles is what a structure determined without
55    /// crystallography carries, and treating it as a real cell would produce
56    /// periodic images of a molecule that was never in a crystal.
57    #[must_use]
58    pub fn is_placeholder(&self) -> bool {
59        values_are_near(&self.lengths, 1.0) && values_are_near(&self.angles, 90.0)
60    }
61}
62
63/// Where the positions of each model live.
64#[derive(Clone, Debug)]
65pub enum CoordinateStore {
66    /// One model.
67    Single(CoordinateBlock),
68    /// Several models over identical topology, so only positions differ.
69    ///
70    /// This covers ensembles and trajectory frames, where annotations are shared
71    /// and repeating them per frame would multiply the memory by the frame count.
72    Dense {
73        /// One block per model, each with the same atom count.
74        frames: Vec<CoordinateBlock>,
75    },
76    /// Models whose atom content genuinely differs.
77    ///
78    /// Padding these into one rectangular block would fabricate atoms that were
79    /// never modelled and destroy the difference between absent and unrecorded,
80    /// so they are held as separate structures instead.
81    Ragged {
82        /// One structure per model.
83        models: Vec<Structure>,
84    },
85}
86
87impl CoordinateStore {
88    pub(crate) fn make_unique_in(
89        &mut self,
90        context: &crate::ExecutionContext,
91    ) -> Result<(), crate::MemoryBudgetError> {
92        match self {
93            Self::Single(block) => block.make_unique_in(context),
94            Self::Dense { frames } => {
95                for frame in frames {
96                    frame.make_unique_in(context)?;
97                }
98                Ok(())
99            }
100            Self::Ragged { .. } => Ok(()),
101        }
102    }
103
104    /// The number of models.
105    #[must_use]
106    pub fn model_count(&self) -> usize {
107        match self {
108            Self::Single(_) => 1,
109            Self::Dense { frames } => frames.len(),
110            Self::Ragged { models } => models.len(),
111        }
112    }
113
114    /// The positions of one model, where they are held directly.
115    ///
116    /// A ragged ensemble answers `None`: its models are structures in their own
117    /// right and are reached through [`CoordinateStore::Ragged`].
118    #[must_use]
119    pub fn block(&self, model: ModelIndex) -> Option<&CoordinateBlock> {
120        match self {
121            Self::Single(block) => (model.get() == 0).then_some(block),
122            Self::Dense { frames } => frames.get(model.as_usize()),
123            // A ragged ensemble's models are structures in their own right, and
124            // a single-model store has no second model to offer.
125            Self::Ragged { .. } => None,
126        }
127    }
128
129    /// Returns true when every model shares one topology.
130    #[must_use]
131    pub const fn is_dense(&self) -> bool {
132        matches!(self, Self::Single(_) | Self::Dense { .. })
133    }
134
135    /// The independent structures of a ragged ensemble.
136    #[must_use]
137    pub fn ragged_models(&self) -> Option<&[Structure]> {
138        match self {
139            Self::Ragged { models } => Some(models),
140            Self::Single(_) | Self::Dense { .. } => None,
141        }
142    }
143}
144
145/// Everything a structure holds.
146#[derive(Clone, Debug)]
147pub struct StructureData {
148    /// What the entry says about itself.
149    pub entry: EntryMetadata,
150    /// The hierarchy.
151    pub topology: Topology,
152    /// The atoms, in chunks.
153    pub chunks: Arc<Vec<AtomChunk>>,
154    /// Chemical connectivity as compact edge columns.
155    pub bonds: crate::bond::BondTable,
156    /// Structure-local typed columns aligned exactly to atom rows.
157    pub annotations: AtomAnnotations,
158    /// Cold, typed domain metadata attached to this snapshot.
159    pub extensions: ExtensionStore,
160    /// The positions.
161    pub coords: CoordinateStore,
162    /// The identifiers this structure interned.
163    pub dictionary: Interner,
164    /// The crystallographic cell, where the file carried one.
165    pub cell: Option<UnitCell>,
166    /// How many times the positions have changed.
167    pub generation: CoordinateGeneration,
168    pub(crate) reservation: Option<Arc<crate::MemoryReservation>>,
169}
170
171/// An immutable structure, shared by reference.
172///
173/// Cloning one is a reference count, not a copy, so passing a structure to
174/// another thread or holding several versions at once is cheap.
175///
176/// # Examples
177///
178/// ```
179/// use molframe_core::structure::{Structure, StructureData};
180///
181/// let structure = Structure::new(StructureData::empty());
182/// assert_eq!(structure.atom_count(), 0);
183/// assert_eq!(structure.model_count(), 1);
184/// ```
185#[repr(C)]
186#[derive(Clone, Debug)]
187pub struct Structure(Arc<StructureData>);
188
189impl StructureData {
190    /// A structure with nothing in it.
191    #[must_use]
192    pub fn empty() -> Self {
193        Self {
194            reservation: None,
195            entry: EntryMetadata::default(),
196            topology: Topology::default(),
197            chunks: Arc::new(Vec::new()),
198            bonds: crate::bond::BondTable::default(),
199            annotations: AtomAnnotations::default(),
200            extensions: ExtensionStore::new(),
201            coords: CoordinateStore::Single(CoordinateBlock::new()),
202            dictionary: Interner::new(),
203            cell: None,
204            generation: CoordinateGeneration::INITIAL,
205        }
206    }
207
208    pub(crate) fn atom_count(&self) -> u32 {
209        match self.chunks.last() {
210            Some(chunk) => chunk.atoms().end,
211            None => 0,
212        }
213    }
214}
215
216impl Structure {
217    /// Wraps structure data for sharing.
218    #[must_use]
219    pub fn new(data: StructureData) -> Self {
220        Self(Arc::new(data))
221    }
222
223    pub(crate) fn retain_reservation(mut self, reservation: Arc<crate::MemoryReservation>) -> Self {
224        let data = Arc::make_mut(&mut self.0);
225        if let CoordinateStore::Ragged { models } = &mut data.coords {
226            for model in models {
227                *model = model.clone().retain_reservation(Arc::clone(&reservation));
228            }
229        }
230        data.reservation = Some(reservation);
231        self
232    }
233
234    /// The data behind the reference.
235    #[must_use]
236    pub fn data(&self) -> &StructureData {
237        &self.0
238    }
239
240    /// The shared handle, for a view that must outlive this reference.
241    #[must_use]
242    pub fn shared(&self) -> Arc<StructureData> {
243        Arc::clone(&self.0)
244    }
245
246    /// The number of atoms in one model.
247    #[must_use]
248    pub fn atom_count(&self) -> u32 {
249        self.0.atom_count()
250    }
251
252    /// A lightweight handle on one atom.
253    #[must_use]
254    pub fn atom(&self, atom: crate::index::AtomIndex) -> Option<super::AtomRef<'_>> {
255        self.0.atom(atom)
256    }
257
258    /// A lightweight handle on one model.
259    #[must_use]
260    pub fn model(&self, model: ModelIndex) -> Option<super::ModelRef<'_>> {
261        self.0.model(model)
262    }
263
264    /// The structure and local model position that represent one model.
265    ///
266    /// Dense models share this structure and retain their position. Ragged
267    /// models are independent single-model structures, so their local position
268    /// is zero. This keeps callers from padding unlike topologies.
269    #[must_use]
270    pub fn model_snapshot(&self, model: ModelIndex) -> Option<(Self, ModelIndex)> {
271        match &self.0.coords {
272            CoordinateStore::Ragged { models } => models
273                .get(model.as_usize())
274                .cloned()
275                .map(|structure| (structure, ModelIndex::new(0))),
276            CoordinateStore::Single(_) | CoordinateStore::Dense { .. } => self
277                .model(model)
278                .map(|_| (self.clone(), model))
279                .or_else(|| {
280                    (model.get() == 0 && self.model_count() == 1).then(|| (self.clone(), model))
281                }),
282        }
283    }
284
285    /// A lightweight handle on one chain.
286    #[must_use]
287    pub fn chain(&self, chain: crate::index::ChainIndex) -> Option<super::ChainRef<'_>> {
288        self.0.chain(chain)
289    }
290
291    /// A lightweight handle on one residue.
292    #[must_use]
293    pub fn residue(&self, residue: crate::index::ResidueIndex) -> Option<super::ResidueRef<'_>> {
294        self.0.residue(residue)
295    }
296
297    /// The number of models.
298    #[must_use]
299    pub fn model_count(&self) -> usize {
300        self.0.coords.model_count()
301    }
302
303    /// The independent model structures when topology differs between models.
304    #[must_use]
305    pub fn ragged_models(&self) -> Option<&[Self]> {
306        self.0.coords.ragged_models()
307    }
308
309    /// The number of chains.
310    #[must_use]
311    pub fn chain_count(&self) -> usize {
312        self.0.topology.chains.len()
313    }
314
315    /// The number of residues.
316    #[must_use]
317    pub fn residue_count(&self) -> usize {
318        self.0.topology.residues.len()
319    }
320
321    /// The number of distinct chemical species.
322    #[must_use]
323    pub fn entity_count(&self) -> usize {
324        self.0.topology.entities.len()
325    }
326
327    /// The positions of the first model, contiguously.
328    ///
329    /// This slice is the buffer itself, not a copy of it, which is what makes
330    /// handing coordinates to a numeric array free.
331    #[must_use]
332    pub fn positions(&self) -> &[[f32; 3]] {
333        match self.model_positions(ModelIndex::new(0)) {
334            Some(positions) => positions,
335            None => &[],
336        }
337    }
338
339    /// The positions of one model.
340    #[must_use]
341    pub fn model_positions(&self, model: ModelIndex) -> Option<&[[f32; 3]]> {
342        self.0.coords.block(model).map(CoordinateBlock::as_slice)
343    }
344
345    /// How many times the positions have changed.
346    #[must_use]
347    pub fn generation(&self) -> CoordinateGeneration {
348        self.0.generation
349    }
350
351    /// Structure-local typed columns aligned to atom rows.
352    #[must_use]
353    pub fn annotations(&self) -> &AtomAnnotations {
354        &self.0.annotations
355    }
356
357    /// Cold, typed domain metadata attached to this snapshot.
358    #[must_use]
359    pub fn extensions(&self) -> &ExtensionStore {
360        &self.0.extensions
361    }
362
363    /// Returns a new snapshot with one typed domain extension attached.
364    #[must_use]
365    pub fn with_extension<T>(&self, key: &'static str, value: T) -> Self
366    where
367        T: std::any::Any + Send + Sync + std::panic::RefUnwindSafe,
368    {
369        let mut data = self.data().clone();
370        data.extensions.insert(key, value);
371        Self::new(data)
372    }
373
374    /// Returns a new snapshot without domain extensions.
375    #[must_use]
376    pub fn without_extensions(&self) -> Self {
377        if self.0.extensions.is_empty() {
378            return self.clone();
379        }
380        let mut data = self.data().clone();
381        data.extensions.clear();
382        Self::new(data)
383    }
384
385    /// The string an interned identifier names.
386    #[must_use]
387    pub fn resolve(&self, symbol: SymbolId) -> Option<&str> {
388        self.0.dictionary.resolve(symbol)
389    }
390}
391
392impl From<StructureData> for Structure {
393    fn from(data: StructureData) -> Self {
394        Self::new(data)
395    }
396}
397
398fn values_are_near(values: &[f64], expected: f64) -> bool {
399    values
400        .iter()
401        .all(|value| (value - expected).abs() < PLACEHOLDER_TOLERANCE)
402}
403
404impl fmt::Display for Structure {
405    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406        write!(
407            f,
408            "Structure(models={}, chains={}, residues={}, atoms={})",
409            self.model_count(),
410            self.chain_count(),
411            self.residue_count(),
412            self.atom_count(),
413        )
414    }
415}
416
417#[cfg(test)]
418#[path = "data_tests.rs"]
419mod tests;