Skip to main content

molframe_core/topology/
chain.rs

1//! The chains of a structure.
2//!
3//! Both identifier namespaces are stored. The file's own normalised label is
4//! always present; the depositor's label is optional, and absent is a different
5//! statement from equal-to-the-label. Choosing between them at read time is what
6//! makes a result impossible to trace back to either the file or the paper, so
7//! nothing here chooses.
8
9use crate::index::{ChainIndex, EntityIndex};
10use crate::limits::{CapacityError, TableError};
11use crate::optional::OptionalSymbol;
12use crate::symbol::SymbolId;
13use std::ops::Range;
14use std::sync::Arc;
15
16/// What kind of polymer a chain is, where it is one.
17#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
18#[non_exhaustive]
19pub enum PolymerKind {
20    /// Not a polymer.
21    #[default]
22    None,
23    /// A polypeptide.
24    Protein,
25    /// Deoxyribonucleic acid.
26    Dna,
27    /// Ribonucleic acid.
28    Rna,
29    /// A chain containing both deoxyribo- and ribonucleotides.
30    NucleicHybrid,
31    /// A polysaccharide.
32    Saccharide,
33    /// A polymer of some other kind.
34    Other,
35}
36
37crate::contract::closed_vocabulary!(
38    PolymerKind,
39    "polymer_kind",
40    [
41        (PolymerKind::None, "none"),
42        (PolymerKind::Protein, "protein"),
43        (PolymerKind::Dna, "dna"),
44        (PolymerKind::Rna, "rna"),
45        (PolymerKind::NucleicHybrid, "nucleic-hybrid"),
46        (PolymerKind::Saccharide, "saccharide"),
47        (PolymerKind::Other, "other"),
48    ]
49);
50
51impl PolymerKind {
52    /// Returns true for any polymer.
53    #[must_use]
54    pub const fn is_polymer(self) -> bool {
55        !matches!(self, Self::None)
56    }
57
58    /// Returns true for a nucleic acid of either kind, or a hybrid.
59    #[must_use]
60    pub const fn is_nucleic(self) -> bool {
61        matches!(self, Self::Dna | Self::Rna | Self::NucleicHybrid)
62    }
63}
64
65/// Everything a chain is created with.
66#[derive(Clone, Copy, Debug)]
67pub struct ChainRecord {
68    /// The normalised chain label.
69    pub label_asym_id: SymbolId,
70    /// The depositor's chain label, if the file carried one.
71    pub auth_asym_id: OptionalSymbol,
72    /// The species this chain instantiates.
73    pub entity: EntityIndex,
74    /// What kind of polymer this chain is, if any.
75    pub polymer_kind: PolymerKind,
76}
77
78/// The chain table.
79#[derive(Clone, Debug, Default)]
80pub struct ChainTable {
81    first_residue: Arc<Vec<u32>>,
82    residue_count: Arc<Vec<u32>>,
83    label_asym_id: Arc<Vec<SymbolId>>,
84    auth_asym_id: Arc<Vec<OptionalSymbol>>,
85    entity: Arc<Vec<EntityIndex>>,
86    polymer_kind: Arc<Vec<PolymerKind>>,
87}
88
89impl ChainTable {
90    /// The number of chains.
91    #[must_use]
92    pub fn len(&self) -> usize {
93        self.label_asym_id.len()
94    }
95
96    /// Returns true when the structure has no chains.
97    #[must_use]
98    pub fn is_empty(&self) -> bool {
99        self.label_asym_id.is_empty()
100    }
101
102    /// Appends a chain covering a range of residues.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`TableError`] for a reversed range or exhausted index space.
107    pub fn push(
108        &mut self,
109        record: ChainRecord,
110        residues: Range<u32>,
111    ) -> Result<ChainIndex, TableError> {
112        let position = u32::try_from(self.label_asym_id.len())
113            .map_err(|_| TableError::Capacity(CapacityError::new("chain table")))?;
114        let residue_count = residues
115            .end
116            .checked_sub(residues.start)
117            .ok_or(TableError::ReversedRange { table: "chain" })?;
118        Arc::make_mut(&mut self.first_residue).push(residues.start);
119        Arc::make_mut(&mut self.residue_count).push(residue_count);
120        Arc::make_mut(&mut self.label_asym_id).push(record.label_asym_id);
121        Arc::make_mut(&mut self.auth_asym_id).push(record.auth_asym_id);
122        Arc::make_mut(&mut self.entity).push(record.entity);
123        Arc::make_mut(&mut self.polymer_kind).push(record.polymer_kind);
124        Ok(ChainIndex::new(position))
125    }
126
127    /// The residues this chain contains.
128    #[must_use]
129    pub fn residues(&self, chain: ChainIndex) -> Option<Range<u32>> {
130        stored_range(&self.first_residue, &self.residue_count, chain.as_usize())
131    }
132
133    /// Finds the chain whose contiguous range contains `residue`.
134    ///
135    /// Chain ranges are ordered, so this is logarithmic in the chain count.
136    ///
137    /// # Panics
138    /// Panics only if the table exceeds the core's `u32` position limit.
139    #[must_use]
140    pub fn containing(&self, residue: u32) -> Option<ChainIndex> {
141        let candidate = self
142            .first_residue
143            .partition_point(|first| *first <= residue)
144            .checked_sub(1)?;
145        let chain = ChainIndex::new(u32::try_from(candidate).ok()?);
146        self.residues(chain)?.contains(&residue).then_some(chain)
147    }
148
149    /// The normalised chain label.
150    #[must_use]
151    pub fn label_asym_id(&self, chain: ChainIndex) -> Option<SymbolId> {
152        self.label_asym_id.get(chain.as_usize()).copied()
153    }
154
155    /// The depositor's chain label, if the file carried one.
156    #[must_use]
157    pub fn auth_asym_id(&self, chain: ChainIndex) -> Option<SymbolId> {
158        self.auth_asym_id
159            .get(chain.as_usize())
160            .copied()
161            .and_then(OptionalSymbol::get)
162    }
163
164    /// Replaces both chain namespaces with one explicit label.
165    ///
166    /// Renaming both prevents a writer from silently preferring the old
167    /// depositor label over the requested new normalised label.
168    ///
169    /// # Errors
170    ///
171    /// Returns [`TableError`] when `chain` is absent.
172    pub fn rename(&mut self, chain: ChainIndex, label: SymbolId) -> Result<(), TableError> {
173        let position = chain.as_usize();
174        if position >= self.label_asym_id.len() || position >= self.auth_asym_id.len() {
175            return Err(TableError::MissingIndex { table: "chain" });
176        }
177        let Some(normalised) = Arc::make_mut(&mut self.label_asym_id).get_mut(position) else {
178            return Err(TableError::MissingIndex { table: "chain" });
179        };
180        *normalised = label;
181        let Some(depositor) = Arc::make_mut(&mut self.auth_asym_id).get_mut(position) else {
182            return Err(TableError::MissingIndex { table: "chain" });
183        };
184        *depositor = OptionalSymbol::some(label);
185        Ok(())
186    }
187
188    /// The species this chain instantiates.
189    #[must_use]
190    pub fn entity(&self, chain: ChainIndex) -> Option<EntityIndex> {
191        self.entity.get(chain.as_usize()).copied()
192    }
193
194    /// What kind of polymer this chain is.
195    #[must_use]
196    pub fn polymer_kind(&self, chain: ChainIndex) -> Option<PolymerKind> {
197        self.polymer_kind.get(chain.as_usize()).copied()
198    }
199
200    /// Reclassifies a chain after component chemistry has been resolved.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`TableError`] when `chain` is absent.
205    pub fn set_polymer_kind(
206        &mut self,
207        chain: ChainIndex,
208        kind: PolymerKind,
209    ) -> Result<(), TableError> {
210        let Some(value) = Arc::make_mut(&mut self.polymer_kind).get_mut(chain.as_usize()) else {
211            return Err(TableError::MissingIndex { table: "chain" });
212        };
213        *value = kind;
214        Ok(())
215    }
216
217    /// Every chain that instantiates `entity`.
218    ///
219    /// This is the copies-of-the-same-molecule question, answered by reading a
220    /// column rather than by comparing sequences.
221    ///
222    pub fn instances_of(&self, entity: EntityIndex) -> impl Iterator<Item = ChainIndex> + '_ {
223        self.entity
224            .iter()
225            .enumerate()
226            .filter(move |(_, chain_entity)| **chain_entity == entity)
227            .filter_map(|(position, _)| u32::try_from(position).ok().map(ChainIndex::new))
228    }
229
230    /// Every chain position.
231    ///
232    pub fn iter(&self) -> impl Iterator<Item = ChainIndex> + '_ {
233        self.label_asym_id
234            .iter()
235            .enumerate()
236            .filter_map(|(position, _)| u32::try_from(position).ok().map(ChainIndex::new))
237    }
238}
239
240fn stored_range(starts: &[u32], counts: &[u32], index: usize) -> Option<Range<u32>> {
241    let start = *starts.get(index)?;
242    let count = *counts.get(index)?;
243
244    Some(start..start.checked_add(count)?)
245}