Skip to main content

molframe_core/topology/
entity.rs

1//! The distinct chemical species a structure contains.
2//!
3//! An entity is not a level of the hierarchy. Several chains may be copies of
4//! one, and no chain contains one, so it lives in its own table with a reference
5//! from each chain. That is what makes "which chains are copies of the same
6//! molecule?" a lookup rather than a sequence comparison, and what lets sequence
7//! and source metadata survive a round trip instead of being repeated per chain
8//! or dropped.
9//!
10//! The canonical sequence held here is the sequence that *should* be present.
11//! The residues in a chain are what was actually modelled, and the difference
12//! between them is the unmodelled region — which is data, not an absence.
13
14use crate::index::EntityIndex;
15use crate::limits::CapacityError;
16use crate::optional::OptionalSymbol;
17use crate::symbol::SymbolId;
18use std::ops::Range;
19use std::sync::Arc;
20
21/// What kind of chemical species an entity is.
22#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
23#[non_exhaustive]
24pub enum EntityKind {
25    /// A polymer: protein, nucleic acid, or a hybrid of the two.
26    Polymer,
27    /// A single chemical component that is not part of a polymer.
28    NonPolymer,
29    /// Water.
30    Water,
31    /// A branched species, such as an oligosaccharide.
32    Branched,
33    /// The file did not say.
34    #[default]
35    Unknown,
36}
37
38crate::contract::closed_vocabulary!(
39    EntityKind,
40    "entity_kind",
41    [
42        (EntityKind::Polymer, "polymer"),
43        (EntityKind::NonPolymer, "non-polymer"),
44        (EntityKind::Water, "water"),
45        (EntityKind::Branched, "branched"),
46        (EntityKind::Unknown, "unknown"),
47    ]
48);
49
50/// The entity table.
51///
52/// Canonical sequences share one pool rather than owning a collection each, so
53/// a structure with three hundred entities holds one allocation for their
54/// sequences instead of three hundred.
55#[derive(Clone, Debug, Default)]
56pub struct EntityTable {
57    kind: Arc<Vec<EntityKind>>,
58    id: Arc<Vec<OptionalSymbol>>,
59    description: Arc<Vec<OptionalSymbol>>,
60    sequence_span: Arc<Vec<Range<u32>>>,
61    sequence_pool: Arc<Vec<SymbolId>>,
62}
63
64impl EntityTable {
65    /// The number of entities.
66    #[must_use]
67    pub fn len(&self) -> usize {
68        self.kind.len()
69    }
70
71    /// Returns true when the structure declares no entities.
72    #[must_use]
73    pub fn is_empty(&self) -> bool {
74        self.kind.is_empty()
75    }
76
77    /// Appends an entity, returning its position.
78    ///
79    /// # Errors
80    ///
81    /// Returns [`CapacityError`] when the entity table exceeds its index space.
82    pub fn push(
83        &mut self,
84        id: SymbolId,
85        kind: EntityKind,
86        description: OptionalSymbol,
87        canonical_sequence: &[SymbolId],
88    ) -> Result<EntityIndex, CapacityError> {
89        self.push_with_optional_id(
90            OptionalSymbol::some(id),
91            kind,
92            description,
93            canonical_sequence,
94        )
95    }
96
97    /// Appends an entity whose source did not declare an identifier.
98    ///
99    /// # Errors
100    ///
101    /// Returns [`CapacityError`] when the entity table exceeds its index space.
102    pub fn push_without_id(
103        &mut self,
104        kind: EntityKind,
105        description: OptionalSymbol,
106        canonical_sequence: &[SymbolId],
107    ) -> Result<EntityIndex, CapacityError> {
108        self.push_with_optional_id(OptionalSymbol::NONE, kind, description, canonical_sequence)
109    }
110
111    fn push_with_optional_id(
112        &mut self,
113        id: OptionalSymbol,
114        kind: EntityKind,
115        description: OptionalSymbol,
116        canonical_sequence: &[SymbolId],
117    ) -> Result<EntityIndex, CapacityError> {
118        let error = || CapacityError::new("entity table");
119        let start = u32::try_from(self.sequence_pool.len()).map_err(|_| error())?;
120        let added = u32::try_from(canonical_sequence.len()).map_err(|_| error())?;
121        let end = start.checked_add(added).ok_or_else(error)?;
122        let position = u32::try_from(self.kind.len()).map_err(|_| error())?;
123        Arc::make_mut(&mut self.sequence_pool).extend_from_slice(canonical_sequence);
124        Arc::make_mut(&mut self.kind).push(kind);
125        Arc::make_mut(&mut self.id).push(id);
126        Arc::make_mut(&mut self.description).push(description);
127        Arc::make_mut(&mut self.sequence_span).push(start..end);
128        Ok(EntityIndex::new(position))
129    }
130
131    /// What kind of species this entity is.
132    #[must_use]
133    pub fn kind(&self, entity: EntityIndex) -> Option<EntityKind> {
134        self.kind.get(entity.as_usize()).copied()
135    }
136
137    /// The identifier the file gave this entity.
138    #[must_use]
139    pub fn id(&self, entity: EntityIndex) -> Option<SymbolId> {
140        self.id
141            .get(entity.as_usize())
142            .copied()
143            .and_then(OptionalSymbol::get)
144    }
145
146    /// The entity carrying `id`, where the file declared one.
147    ///
148    /// Entity tables are normally small and this is used while building one
149    /// chain at a time, not in an atom loop. Keeping the lookup here avoids a
150    /// second index whose lifetime and invalidation would duplicate the table.
151    ///
152    #[must_use]
153    pub fn find_by_id(&self, id: SymbolId) -> Option<EntityIndex> {
154        self.id
155            .iter()
156            .position(|candidate| candidate.get() == Some(id))
157            .and_then(|position| u32::try_from(position).ok())
158            .map(EntityIndex::new)
159    }
160
161    /// The entity's description, if the file carried one.
162    #[must_use]
163    pub fn description(&self, entity: EntityIndex) -> Option<SymbolId> {
164        self.description
165            .get(entity.as_usize())
166            .copied()
167            .and_then(OptionalSymbol::get)
168    }
169
170    /// The sequence this entity should have, one component per position.
171    ///
172    /// Compare it against the residues a chain actually holds to find what was
173    /// not modelled.
174    #[must_use]
175    pub fn canonical_sequence(&self, entity: EntityIndex) -> &[SymbolId] {
176        let Some(span) = self.sequence_span.get(entity.as_usize()) else {
177            return &[];
178        };
179
180        sequence_slice(&self.sequence_pool, span)
181    }
182
183    /// Every entity position.
184    ///
185    pub fn iter(&self) -> impl Iterator<Item = EntityIndex> + '_ {
186        self.kind
187            .iter()
188            .enumerate()
189            .filter_map(|(position, _)| u32::try_from(position).ok().map(EntityIndex::new))
190    }
191}
192
193fn sequence_slice<'a>(pool: &'a [SymbolId], span: &Range<u32>) -> &'a [SymbolId] {
194    let Ok(start) = usize::try_from(span.start) else {
195        return &[];
196    };
197    let Ok(end) = usize::try_from(span.end) else {
198        return &[];
199    };
200
201    match pool.get(start..end) {
202        Some(sequence) => sequence,
203        None => &[],
204    }
205}