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