Skip to main content

molframe_core/topology/
residue.rs

1//! The residues of a structure.
2//!
3//! A residue is identified by its chain, its number, its insertion code and its
4//! component. The insertion code is part of that identity rather than an
5//! annotation on it: 163, 163A and 163B are three residues, and an antibody
6//! numbering scheme depends on that entirely.
7
8use super::csr::stored_range;
9use crate::column::BitVec;
10use crate::index::ResidueIndex;
11use crate::limits::{CapacityError, TableError};
12use crate::optional::{OptionalI32, OptionalSymbol};
13use crate::symbol::SymbolId;
14use std::ops::Range;
15use std::sync::Arc;
16
17/// The residues of a structure.
18#[derive(Clone, Debug, Default)]
19pub struct ResidueTable {
20    first_atom: Arc<Vec<u32>>,
21    atom_count: Arc<Vec<u32>>,
22    label_comp_id: Arc<Vec<SymbolId>>,
23    auth_comp_id: Arc<Vec<OptionalSymbol>>,
24    label_seq_id: Arc<Vec<OptionalI32>>,
25    auth_seq_id: Arc<Vec<OptionalI32>>,
26    ins_code: Arc<Vec<OptionalSymbol>>,
27    het: BitVec,
28}
29
30/// Everything a residue is created with.
31#[derive(Clone, Copy, Debug)]
32pub struct ResidueRecord {
33    /// The normalised component code.
34    pub label_comp_id: SymbolId,
35    /// The depositor's component code, if the file carried one.
36    pub auth_comp_id: OptionalSymbol,
37    /// The sequence position within the entity. Absent for non-polymers.
38    pub label_seq_id: OptionalI32,
39    /// The depositor's residue number.
40    pub auth_seq_id: OptionalI32,
41    /// The insertion code, which is part of identity rather than an annotation.
42    pub ins_code: OptionalSymbol,
43    /// Whether the file recorded this residue as a heterogen.
44    pub het: bool,
45}
46
47impl ResidueTable {
48    /// The number of residues.
49    #[must_use]
50    pub fn len(&self) -> usize {
51        self.label_comp_id.len()
52    }
53
54    /// Returns true when the structure has no residues.
55    #[must_use]
56    pub fn is_empty(&self) -> bool {
57        self.label_comp_id.is_empty()
58    }
59
60    /// Appends a residue covering a range of atoms.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`TableError`] for a reversed range or exhausted index space.
65    pub fn push(
66        &mut self,
67        record: ResidueRecord,
68        atoms: Range<u32>,
69    ) -> Result<ResidueIndex, TableError> {
70        let capacity = || TableError::Capacity(CapacityError::new("residue table"));
71        let position = u32::try_from(self.label_comp_id.len()).map_err(|_| capacity())?;
72        let atom_count = atoms
73            .end
74            .checked_sub(atoms.start)
75            .ok_or(TableError::ReversedRange { table: "residue" })?;
76        self.het.try_push(record.het).map_err(|_| capacity())?;
77        Arc::make_mut(&mut self.first_atom).push(atoms.start);
78        Arc::make_mut(&mut self.atom_count).push(atom_count);
79        Arc::make_mut(&mut self.label_comp_id).push(record.label_comp_id);
80        Arc::make_mut(&mut self.auth_comp_id).push(record.auth_comp_id);
81        Arc::make_mut(&mut self.label_seq_id).push(record.label_seq_id);
82        Arc::make_mut(&mut self.auth_seq_id).push(record.auth_seq_id);
83        Arc::make_mut(&mut self.ins_code).push(record.ins_code);
84        Ok(ResidueIndex::new(position))
85    }
86
87    /// Replaces the atoms a residue covers.
88    ///
89    /// A reader that discovers a residue's extent only after reading its last
90    /// atom needs this; nothing else should reach for it, because moving a
91    /// residue's atoms without moving its neighbours' breaks the tiling.
92    ///
93    /// # Errors
94    ///
95    /// Returns [`TableError`] for a reversed range or absent residue index.
96    pub fn set_atoms(
97        &mut self,
98        residue: ResidueIndex,
99        atoms: Range<u32>,
100    ) -> Result<(), TableError> {
101        let atom_count = atoms
102            .end
103            .checked_sub(atoms.start)
104            .ok_or(TableError::ReversedRange { table: "residue" })?;
105        let Some(first) = Arc::make_mut(&mut self.first_atom).get_mut(residue.as_usize()) else {
106            return Err(TableError::MissingIndex { table: "residue" });
107        };
108        *first = atoms.start;
109        if let Some(count) = Arc::make_mut(&mut self.atom_count).get_mut(residue.as_usize()) {
110            *count = atom_count;
111            return Ok(());
112        }
113        Err(TableError::MissingIndex { table: "residue" })
114    }
115
116    pub(crate) fn last_atom_end(&self) -> Option<u32> {
117        self.first_atom
118            .last()
119            .copied()?
120            .checked_add(self.atom_count.last().copied()?)
121    }
122
123    /// The atoms this residue contains.
124    #[must_use]
125    pub fn atoms(&self, residue: ResidueIndex) -> Option<Range<u32>> {
126        stored_range(&self.first_atom, &self.atom_count, residue.as_usize())
127    }
128
129    /// The normalised component code.
130    #[must_use]
131    pub fn label_comp_id(&self, residue: ResidueIndex) -> Option<SymbolId> {
132        self.label_comp_id.get(residue.as_usize()).copied()
133    }
134
135    /// The depositor's component code, if the file carried one.
136    #[must_use]
137    pub fn auth_comp_id(&self, residue: ResidueIndex) -> Option<SymbolId> {
138        self.auth_comp_id
139            .get(residue.as_usize())
140            .copied()
141            .and_then(OptionalSymbol::get)
142    }
143
144    /// The sequence position within the entity, absent for a non-polymer.
145    #[must_use]
146    pub fn label_seq_id(&self, residue: ResidueIndex) -> Option<i32> {
147        self.label_seq_id
148            .get(residue.as_usize())
149            .copied()
150            .and_then(OptionalI32::get)
151    }
152
153    /// Replaces the sequence position of one residue.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`TableError`] when the residue does not exist.
158    pub fn set_label_seq_id(&mut self, residue: ResidueIndex, seq: i32) -> Result<(), TableError> {
159        let slot = Arc::make_mut(&mut self.label_seq_id)
160            .get_mut(residue.as_usize())
161            .ok_or(TableError::MissingIndex { table: "residue" })?;
162        *slot = OptionalI32::some(seq);
163        Ok(())
164    }
165
166    /// The depositor's residue number.
167    #[must_use]
168    pub fn auth_seq_id(&self, residue: ResidueIndex) -> Option<i32> {
169        self.auth_seq_id
170            .get(residue.as_usize())
171            .copied()
172            .and_then(OptionalI32::get)
173    }
174
175    /// The insertion code.
176    ///
177    /// Part of identity: `163`, `163A` and `163B` are three residues, and an
178    /// antibody numbering scheme depends on that entirely.
179    #[must_use]
180    pub fn ins_code(&self, residue: ResidueIndex) -> Option<SymbolId> {
181        self.ins_code
182            .get(residue.as_usize())
183            .copied()
184            .and_then(OptionalSymbol::get)
185    }
186
187    /// Whether the file recorded this residue as a heterogen.
188    #[must_use]
189    pub fn is_het(&self, residue: ResidueIndex) -> bool {
190        self.het.test(residue.get())
191    }
192
193    /// The residue containing `atom`, found by searching the offsets.
194    ///
195    /// The offsets ascend, so this is a binary search. Chunks carry a denser
196    /// mapping for the case where this question is asked per atom.
197    #[must_use]
198    pub fn containing(&self, atom: u32) -> Option<ResidueIndex> {
199        let insertion = self.first_atom.partition_point(|first| *first <= atom);
200
201        let candidate = insertion.checked_sub(1)?;
202        let first = *self.first_atom.get(candidate)?;
203        let count = *self.atom_count.get(candidate)?;
204        let end = first.checked_add(count)?;
205
206        if atom < first || atom >= end {
207            return None;
208        }
209
210        let position = u32::try_from(candidate).ok()?;
211
212        Some(ResidueIndex::new(position))
213    }
214}