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