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