Skip to main content

molframe_core/annotation/
model.rs

1//! Typed structure-local columns attached to atom rows.
2
3use crate::column::{Presence, ValidityMask};
4use crate::limits::CapacityError;
5use crate::symbol::SymbolId;
6use std::collections::BTreeMap;
7use std::sync::Arc;
8
9/// Conventional custom column name for atom segment identifiers.
10pub const SEGMENT_ID_ANNOTATION: &str = "segid";
11/// Conventional custom column name for partial atomic charge.
12pub const PARTIAL_CHARGE_ANNOTATION: &str = "charge";
13/// Conventional custom column name for per-atom PQR radii.
14pub const ATOM_RADIUS_ANNOTATION: &str = "radius";
15/// Conventional custom column name for `AutoDock` atom types.
16pub const AUTODOCK_TYPE_ANNOTATION: &str = "autodock_type";
17/// Component role assigned from a chemical component definition.
18pub const COMPONENT_KIND_ANNOTATION: &str = "component_kind";
19/// Explicit polymer atom role assigned from a CCD-aware or caller-supplied profile.
20pub const POLYMER_ATOM_ROLE_ANNOTATION: &str = "polymer_atom_role";
21/// Aromatic-atom flag assigned from a chemical component definition.
22pub const AROMATIC_ATOM_ANNOTATION: &str = "aromatic";
23/// Formal charge assigned from a chemical component definition.
24pub const FORMAL_CHARGE_ANNOTATION: &str = "formalcharge";
25/// Hydrogen-bond donor role perceived from CCD connectivity and protonation.
26pub const HBOND_DONOR_ANNOTATION: &str = "hbond_donor";
27/// Hydrogen-bond acceptor role perceived from CCD connectivity and protonation.
28pub const HBOND_ACCEPTOR_ANNOTATION: &str = "hbond_acceptor";
29/// Modelled stereochemical configuration anchored to component coordinates.
30pub const STEREO_CONFIGURATION_ANNOTATION: &str = "chirality";
31/// Conventional custom column name for per-atom predicted confidence.
32pub const PLDDT_ANNOTATION: &str = "plddt";
33/// Conventional custom column name for per-atom predicted aligned error.
34pub const PAE_ANNOTATION: &str = "pae";
35/// Per-atom stable identifier of the materialised chain copy an atom belongs to.
36pub const INSTANCE_ID_ANNOTATION: &str = "molframe.instance_id";
37
38/// One immutable typed annotation column with three-state validity.
39#[derive(Clone, Debug)]
40pub struct AnnotationColumn<T> {
41    values: Arc<Vec<T>>,
42    validity: ValidityMask,
43}
44
45impl<T> AnnotationColumn<T> {
46    /// Builds an all-present column.
47    ///
48    /// # Errors
49    ///
50    /// Returns [`CapacityError`] when the column exceeds the supported row count.
51    pub fn from_values(values: Vec<T>) -> Result<Self, CapacityError> {
52        let len =
53            u32::try_from(values.len()).map_err(|_| CapacityError::new("annotation column"))?;
54        let validity = ValidityMask::all_present(len);
55        Ok(Self {
56            values: Arc::new(values),
57            validity,
58        })
59    }
60
61    /// Number of atom rows covered by the column.
62    #[must_use]
63    pub fn len(&self) -> u32 {
64        self.validity.len()
65    }
66
67    /// Returns true when the column covers no rows.
68    #[must_use]
69    pub fn is_empty(&self) -> bool {
70        self.validity.is_empty()
71    }
72
73    /// Validity state at one atom row.
74    #[must_use]
75    pub fn presence(&self, atom: u32) -> Presence {
76        self.validity.get(atom)
77    }
78
79    /// Dense values backing this column.
80    #[must_use]
81    pub fn values(&self) -> &[T] {
82        &self.values
83    }
84
85    pub(crate) fn filter(&self, keep: impl Fn(u32) -> bool) -> Result<Self, CapacityError>
86    where
87        T: Clone,
88    {
89        let entries = self
90            .values
91            .iter()
92            .enumerate()
93            .filter_map(|(position, value)| {
94                let position = u32::try_from(position).ok()?;
95                keep(position).then(|| (value.clone(), self.validity.get(position)))
96            });
97        Self::from_entries(entries)
98    }
99}
100
101impl<T> AnnotationColumn<T> {
102    /// Builds a column from dense values paired with their validity state.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`CapacityError`] when the column exceeds the supported row count.
107    pub fn from_entries(
108        entries: impl IntoIterator<Item = (T, Presence)>,
109    ) -> Result<Self, CapacityError> {
110        let (values, presences): (Vec<_>, Vec<_>) = entries.into_iter().unzip();
111        let validity = ValidityMask::try_from_iter(presences)
112            .ok_or_else(|| CapacityError::new("annotation column"))?;
113        Ok(Self {
114            values: Arc::new(values),
115            validity,
116        })
117    }
118}
119
120impl<T: Copy> AnnotationColumn<T> {
121    /// Value and validity at one row, or `None` outside the column.
122    #[must_use]
123    pub fn get(&self, atom: u32) -> Option<(T, Presence)> {
124        self.values
125            .get(atom as usize)
126            .copied()
127            .map(|value| (value, self.validity.get(atom)))
128    }
129}
130
131/// The supported physical types for a custom atom annotation.
132#[derive(Clone, Debug)]
133#[non_exhaustive]
134pub enum AtomAnnotation {
135    /// Boolean values.
136    Boolean(AnnotationColumn<bool>),
137    /// Signed 64-bit integers.
138    Integer(AnnotationColumn<i64>),
139    /// IEEE-754 64-bit real values.
140    Real(AnnotationColumn<f64>),
141    /// Dictionary-backed categorical or textual values.
142    Symbol(AnnotationColumn<SymbolId>),
143}
144
145impl AtomAnnotation {
146    /// Number of atom rows covered by this column.
147    #[must_use]
148    pub fn len(&self) -> u32 {
149        match self {
150            Self::Boolean(column) => column.len(),
151            Self::Integer(column) => column.len(),
152            Self::Real(column) => column.len(),
153            Self::Symbol(column) => column.len(),
154        }
155    }
156
157    /// Returns true when the column covers no rows.
158    #[must_use]
159    pub fn is_empty(&self) -> bool {
160        self.len() == 0
161    }
162
163    pub(crate) fn filter(&self, keep: impl Fn(u32) -> bool + Copy) -> Result<Self, CapacityError> {
164        Ok(match self {
165            Self::Boolean(column) => Self::Boolean(column.filter(keep)?),
166            Self::Integer(column) => Self::Integer(column.filter(keep)?),
167            Self::Real(column) => Self::Real(column.filter(keep)?),
168            Self::Symbol(column) => Self::Symbol(column.filter(keep)?),
169        })
170    }
171}
172
173/// Custom per-atom columns, ordered by name for deterministic iteration.
174#[derive(Clone, Debug, Default)]
175pub struct AtomAnnotations {
176    columns: BTreeMap<Box<str>, AtomAnnotation>,
177}
178
179impl AtomAnnotations {
180    /// Number of custom columns.
181    #[must_use]
182    pub fn len(&self) -> usize {
183        self.columns.len()
184    }
185
186    /// Returns true when no custom column is present.
187    #[must_use]
188    pub fn is_empty(&self) -> bool {
189        self.columns.is_empty()
190    }
191
192    /// Looks up a column by its exact name.
193    #[must_use]
194    pub fn get(&self, name: &str) -> Option<&AtomAnnotation> {
195        self.columns.get(name)
196    }
197
198    /// Iterates columns in lexicographic name order.
199    pub fn iter(&self) -> impl Iterator<Item = (&str, &AtomAnnotation)> {
200        self.columns
201            .iter()
202            .map(|(name, column)| (name.as_ref(), column))
203    }
204
205    /// Inserts or replaces a column, returning the previous one.
206    pub fn insert(
207        &mut self,
208        name: impl Into<Box<str>>,
209        column: AtomAnnotation,
210    ) -> Option<AtomAnnotation> {
211        self.columns.insert(name.into(), column)
212    }
213
214    /// Removes a named column.
215    pub fn remove(&mut self, name: &str) -> Option<AtomAnnotation> {
216        self.columns.remove(name)
217    }
218
219    pub(crate) fn filter(&self, keep: impl Fn(u32) -> bool + Copy) -> Result<Self, CapacityError> {
220        let columns = self
221            .columns
222            .iter()
223            .map(|(name, column)| Ok((name.clone(), column.filter(keep)?)))
224            .collect::<Result<_, CapacityError>>()?;
225        Ok(Self { columns })
226    }
227}
228
229#[cfg(test)]
230#[path = "model_tests.rs"]
231mod tests;