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
37impl PolymerKind {
38    /// Returns true for any polymer.
39    #[must_use]
40    pub const fn is_polymer(self) -> bool {
41        !matches!(self, Self::None)
42    }
43
44    /// Returns true for a nucleic acid of either kind, or a hybrid.
45    #[must_use]
46    pub const fn is_nucleic(self) -> bool {
47        matches!(self, Self::Dna | Self::Rna | Self::NucleicHybrid)
48    }
49}
50
51/// Everything a chain is created with.
52#[derive(Clone, Copy, Debug)]
53pub struct ChainRecord {
54    /// The normalised chain label.
55    pub label_asym_id: SymbolId,
56    /// The depositor's chain label, if the file carried one.
57    pub auth_asym_id: OptionalSymbol,
58    /// The species this chain instantiates.
59    pub entity: EntityIndex,
60    /// What kind of polymer this chain is, if any.
61    pub polymer_kind: PolymerKind,
62}
63
64/// The chain table.
65#[derive(Clone, Debug, Default)]
66pub struct ChainTable {
67    first_residue: Arc<Vec<u32>>,
68    residue_count: Arc<Vec<u32>>,
69    label_asym_id: Arc<Vec<SymbolId>>,
70    auth_asym_id: Arc<Vec<OptionalSymbol>>,
71    entity: Arc<Vec<EntityIndex>>,
72    polymer_kind: Arc<Vec<PolymerKind>>,
73}
74
75impl ChainTable {
76    /// The number of chains.
77    #[must_use]
78    pub fn len(&self) -> usize {
79        self.label_asym_id.len()
80    }
81
82    /// Returns true when the structure has no chains.
83    #[must_use]
84    pub fn is_empty(&self) -> bool {
85        self.label_asym_id.is_empty()
86    }
87
88    /// Appends a chain covering a range of residues.
89    ///
90    /// # Errors
91    ///
92    /// Returns [`TableError`] for a reversed range or exhausted index space.
93    pub fn push(
94        &mut self,
95        record: ChainRecord,
96        residues: Range<u32>,
97    ) -> Result<ChainIndex, TableError> {
98        let position = u32::try_from(self.label_asym_id.len())
99            .map_err(|_| TableError::Capacity(CapacityError::new("chain table")))?;
100        let residue_count = residues
101            .end
102            .checked_sub(residues.start)
103            .ok_or(TableError::ReversedRange { table: "chain" })?;
104        Arc::make_mut(&mut self.first_residue).push(residues.start);
105        Arc::make_mut(&mut self.residue_count).push(residue_count);
106        Arc::make_mut(&mut self.label_asym_id).push(record.label_asym_id);
107        Arc::make_mut(&mut self.auth_asym_id).push(record.auth_asym_id);
108        Arc::make_mut(&mut self.entity).push(record.entity);
109        Arc::make_mut(&mut self.polymer_kind).push(record.polymer_kind);
110        Ok(ChainIndex::new(position))
111    }
112
113    /// The residues this chain contains.
114    #[must_use]
115    pub fn residues(&self, chain: ChainIndex) -> Option<Range<u32>> {
116        stored_range(&self.first_residue, &self.residue_count, chain.as_usize())
117    }
118
119    /// Finds the chain whose contiguous range contains `residue`.
120    ///
121    /// Chain ranges are ordered, so this is logarithmic in the chain count.
122    ///
123    /// # Panics
124    /// Panics only if the table exceeds the core's `u32` position limit.
125    #[must_use]
126    pub fn containing(&self, residue: u32) -> Option<ChainIndex> {
127        let candidate = self
128            .first_residue
129            .partition_point(|first| *first <= residue)
130            .checked_sub(1)?;
131        let chain = ChainIndex::new(u32::try_from(candidate).ok()?);
132        self.residues(chain)?.contains(&residue).then_some(chain)
133    }
134
135    /// The normalised chain label.
136    #[must_use]
137    pub fn label_asym_id(&self, chain: ChainIndex) -> Option<SymbolId> {
138        self.label_asym_id.get(chain.as_usize()).copied()
139    }
140
141    /// The depositor's chain label, if the file carried one.
142    #[must_use]
143    pub fn auth_asym_id(&self, chain: ChainIndex) -> Option<SymbolId> {
144        self.auth_asym_id
145            .get(chain.as_usize())
146            .copied()
147            .and_then(OptionalSymbol::get)
148    }
149
150    /// Replaces both chain namespaces with one explicit label.
151    ///
152    /// Renaming both prevents a writer from silently preferring the old
153    /// depositor label over the requested new normalised label.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`TableError`] when `chain` is absent.
158    pub fn rename(&mut self, chain: ChainIndex, label: SymbolId) -> Result<(), TableError> {
159        let position = chain.as_usize();
160        if position >= self.label_asym_id.len() || position >= self.auth_asym_id.len() {
161            return Err(TableError::MissingIndex { table: "chain" });
162        }
163        let Some(normalised) = Arc::make_mut(&mut self.label_asym_id).get_mut(position) else {
164            return Err(TableError::MissingIndex { table: "chain" });
165        };
166        *normalised = label;
167        let Some(depositor) = Arc::make_mut(&mut self.auth_asym_id).get_mut(position) else {
168            return Err(TableError::MissingIndex { table: "chain" });
169        };
170        *depositor = OptionalSymbol::some(label);
171        Ok(())
172    }
173
174    /// The species this chain instantiates.
175    #[must_use]
176    pub fn entity(&self, chain: ChainIndex) -> Option<EntityIndex> {
177        self.entity.get(chain.as_usize()).copied()
178    }
179
180    /// What kind of polymer this chain is.
181    #[must_use]
182    pub fn polymer_kind(&self, chain: ChainIndex) -> Option<PolymerKind> {
183        self.polymer_kind.get(chain.as_usize()).copied()
184    }
185
186    /// Reclassifies a chain after component chemistry has been resolved.
187    ///
188    /// # Errors
189    ///
190    /// Returns [`TableError`] when `chain` is absent.
191    pub fn set_polymer_kind(
192        &mut self,
193        chain: ChainIndex,
194        kind: PolymerKind,
195    ) -> Result<(), TableError> {
196        let Some(value) = Arc::make_mut(&mut self.polymer_kind).get_mut(chain.as_usize()) else {
197            return Err(TableError::MissingIndex { table: "chain" });
198        };
199        *value = kind;
200        Ok(())
201    }
202
203    /// Every chain that instantiates `entity`.
204    ///
205    /// This is the copies-of-the-same-molecule question, answered by reading a
206    /// column rather than by comparing sequences.
207    ///
208    pub fn instances_of(&self, entity: EntityIndex) -> impl Iterator<Item = ChainIndex> + '_ {
209        self.entity
210            .iter()
211            .enumerate()
212            .filter(move |(_, chain_entity)| **chain_entity == entity)
213            .filter_map(|(position, _)| u32::try_from(position).ok().map(ChainIndex::new))
214    }
215
216    /// Every chain position.
217    ///
218    pub fn iter(&self) -> impl Iterator<Item = ChainIndex> + '_ {
219        self.label_asym_id
220            .iter()
221            .enumerate()
222            .filter_map(|(position, _)| u32::try_from(position).ok().map(ChainIndex::new))
223    }
224}
225
226fn stored_range(starts: &[u32], counts: &[u32], index: usize) -> Option<Range<u32>> {
227    let start = *starts.get(index)?;
228    let count = *counts.get(index)?;
229
230    Some(start..start.checked_add(count)?)
231}