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;