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