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